@perspectivefi/addresses 0.1.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@ The registry decides whether something _may_ go to prod, not whether it _is_ lis
8
8
 
9
9
  Token addresses stay in `token-list`. Pools, PTs and vaults are not listed one by one (see [Pools inherit from factories](#pools-inherit-from-factories)).
10
10
 
11
+ What `safe` means and who may approve it: [POLICY.md](./POLICY.md). What this package does and does not guarantee: [Guarantees](#guarantees).
12
+
11
13
  ## The four statuses
12
14
 
13
15
  | Status | What it means | Allowed on QA | Allowed in prod |
@@ -39,8 +41,15 @@ One file per chain in `registry/`: `registry/<chainId>.json` (e.g. `1.json`, `84
39
41
  "audit": "https://...",
40
42
  "deployer": "0x...",
41
43
  "inherit": true,
44
+ "approval": { "basis": "audit", "pr": 12, "note": "..." },
42
45
  "notes": "..."
43
46
  }
47
+ ],
48
+ "denylist": [
49
+ {
50
+ "address": "0x...",
51
+ "reason": "test vault removed from the registry; must never be listed in prod"
52
+ }
44
53
  ]
45
54
  }
46
55
  ```
@@ -55,24 +64,41 @@ One file per chain in `registry/`: `registry/<chainId>.json` (e.g. `1.json`, `84
55
64
  | `audit` | Link to the audit report. Optional for `unsafe` and `external`. |
56
65
  | `deployer` | Optional deployer EOA. The nightly and PR role check make sure it holds no admin role on `safe`. |
57
66
  | `inherit` | Only on `factory` / `beacon`: instances created by it inherit its status. |
67
+ | `approval` | `{ basis, pr, note? }`. **Required on every `safe` entry**, see [POLICY.md](./POLICY.md). |
68
+ | `denylist` | Per chain file, optional: `{ address, reason }` items. These addresses are never acceptable. |
69
+
70
+ `approval.basis` is one of `audit`, `prod-baseline`, `external-integration`, `governance`; `approval.pr` is the registry PR that approved the entry. `approval` is not part of the runtime data, only of `dist/registry.json`.
71
+
72
+ ### Denylist
73
+
74
+ A `blocked` entry must never simply be deleted: once it is gone the address is just "unknown" (or, behind a `safe` factory, looks inherited-safe). Move it to the chain's `denylist` instead. A denylisted address:
75
+
76
+ - is `blocked` for `statusOf(chain, address)` even without an entry, and `isDenied(chain, address)` is `true`;
77
+ - is shipped in both the full and the `/prod` datasets, so consumers can still refuse it;
78
+ - fails the build check wherever it appears, on that chain;
79
+ - cannot also be an entry (validation error); addresses must be valid, unique and have a non-empty `reason`.
80
+
81
+ CI enforces this: `npm run validate -- --base origin/main` fails when a `blocked` entry of the base branch has disappeared and is not in the denylist.
58
82
 
59
83
  ## How to add an address
60
84
 
61
- 1. Add an entry to `registry/<chain>.json` with status `unsafe`. Use the checksummed address. If the chain has no file yet, create it (name = chain id) and fill `governance`.
85
+ 1. Add an entry to `registry/<chain>.json` with status `unsafe` (never straight to `safe`: approval is a separate PR, see [POLICY.md](./POLICY.md)). Use the checksummed address. If the chain has no file yet, create it (name = chain id) and fill `governance`.
62
86
  2. Run `npm run validate`, and `npm run onchain` if you want to see the chain check locally.
63
87
  3. Open a PR. CI runs the checks below; you need 2 approvals.
64
88
 
65
- Run `npm run generate` after editing a registry file and commit `src/generated/registry.ts` as well. CI fails if it is stale.
89
+ Run `npm run generate` after editing a registry file and commit the files in `src/generated/` as well. CI fails if it is stale.
66
90
 
67
91
  ## How to promote a status
68
92
 
69
93
  Each step is a PR that changes one `status` line (plus the fields it needs).
70
94
 
71
- | Change | What you add | What CI checks |
72
- | ------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
73
- | `unsafe` -> `role-unsafe` | `source.repo` + 40-hex `source.commit`, `audit` URL | The fields exist and are well-formed. The contract has bytecode on-chain. |
74
- | `role-unsafe` -> `safe` | Hand admin roles to the DAO, then change status | Everything above, plus the **on-chain role check**: every owner / admin found is in `governance`, and the `deployer` (if set) holds no admin role. |
75
- | anything -> `blocked` | A reason in `notes` | The contract has bytecode. |
95
+ | Change | What you add | What CI checks |
96
+ | ------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
97
+ | `unsafe` -> `role-unsafe` | `source.repo` + 40-hex `source.commit`, `audit` URL | The fields exist and are well-formed. The contract has bytecode on-chain. |
98
+ | `role-unsafe` -> `safe` | Hand admin roles to the DAO, then change status, add `approval` | Everything above, plus `approval.pr` equal to this PR's number, plus the **on-chain role check**: every owner / admin found is in `governance`, and the `deployer` (if set) holds no admin role. |
99
+ | anything -> `blocked` | A reason in `notes` | The contract has bytecode. |
100
+
101
+ `source` and `audit` are required for `role-unsafe`, and for `safe` unless the entry's `approval.basis` is something other than `audit` (`prod-baseline`, `external-integration`, `governance`; the evidence is then in the PR, see [POLICY.md](./POLICY.md)).
76
102
 
77
103
  The on-chain role check reads `owner()`, the EIP-1967 admin slot (and the owner of that admin contract if it is one), and `authority()` for OpenZeppelin AccessManager (`hasRole(0, governance[i])` must be true for at least one governance address, and `hasRole(0, deployer)` must be false). RPC failures on a `safe` entry fail the run.
78
104
 
@@ -81,6 +107,8 @@ Every other check, on every PR:
81
107
  - JSON shape and types, `chain` matches the file name, keys are well-formed and unique per chain.
82
108
  - EVM addresses are EIP-55 checksummed, Stellar ids are valid, `governance` addresses are valid.
83
109
  - `inherit` only on factories / beacons.
110
+ - `approval` is well-formed and present on every `safe` entry; in a PR, an entry that is newly `safe` must carry this PR's number in `approval.pr`.
111
+ - `denylist` is valid (see above) and no blocked entry was deleted compared with the base branch.
84
112
  - The same address under two keys gives a warning, not a failure.
85
113
  - Every address has bytecode on-chain (Stellar is not checked yet).
86
114
 
@@ -90,7 +118,7 @@ Every other check, on every PR:
90
118
 
91
119
  There are hundreds of pools, PTs and vaults and new ones appear all the time, so they are not listed. They inherit the status of the factory that created them: a pool made by a `safe` factory is `safe`. Mark a factory or beacon with `"inherit": true` to make that explicit.
92
120
 
93
- If someone creates a test pool through a prod factory, add that single pool to the registry as `blocked` (`kind: "instance"`). A `blocked` entry always wins over the factory's status.
121
+ If someone creates a test pool through a prod factory, add that single pool to the registry as `blocked` (`kind: "instance"`). A `blocked` entry, and a denylisted address, always win over the factory's status. Runtime data carries `inherit: true` on the factories / beacons that opted in; the package only exposes the data, consumers decide how to verify provenance.
94
122
 
95
123
  ## Using it in an app
96
124
 
@@ -107,23 +135,39 @@ requireAddress(1, 'otc.engine') // throws "no address for key ... on chain 1" in
107
135
 
108
136
  Pick the entry point at build time (alias `@perspectivefi/addresses` to `.../prod` in production builds). Both have the same API:
109
137
 
110
- | Function | Returns |
111
- | ----------------------------- | ------------------------------------------------------ |
112
- | `address(chain, key)` | the address or `undefined` |
113
- | `requireAddress(chain, key)` | the address or throws an error naming chain and key |
114
- | `entry(chain, key)` | the full `Entry` or `undefined` |
115
- | `findByAddress(chain, addr)` | the `Entry` (EVM compare is case-insensitive) |
116
- | `statusOf(chain, addr)` | `Status` or `undefined` |
117
- | `isProdSafe(chain, addr)` | `true` only for `safe` |
118
- | `entries(chain?)`, `chains()` | all entries (optionally one chain), the chains present |
138
+ | Function | Returns |
139
+ | ----------------------------- | -------------------------------------------------------------- |
140
+ | `address(chain, key)` | the address or `undefined` |
141
+ | `requireAddress(chain, key)` | the address or throws an error naming chain and key |
142
+ | `entry(chain, key)` | the full `Entry` or `undefined` |
143
+ | `findByAddress(chain, addr)` | the `Entry` (EVM compare is case-insensitive) |
144
+ | `statusOf(chain, addr)` | `Status`, `blocked` for a denylisted address, else `undefined` |
145
+ | `isDenied(chain, addr)` | `true` when the address is on that chain's denylist |
146
+ | `isProdSafe(chain, addr)` | `true` only for `safe` |
147
+ | `entries(chain?)`, `chains()` | all entries (optionally one chain), the chains present |
119
148
 
120
149
  Types: `Entry`, `Status`, `Kind`, `ChainRef` (chain ids + `'stellar'`), `AddressKey` (every key in the registry). `chain` also accepts a plain `number`, e.g. a runtime chain id.
121
150
 
122
- The package has zero runtime dependencies. The `/prod` bundle contains no non-safe address at all (a test checks `dist/prod.*`).
151
+ The package has zero runtime dependencies. The `/prod` bundle contains no non-safe entry at all (a test checks `dist/prod.*`); the only non-safe addresses in it are the denylisted ones, which are there so they can be refused.
152
+
153
+ ### Per-key exports (tree-shakeable)
154
+
155
+ For front-ends, import only the keys you need. Unused keys are dropped by the bundler, so only those addresses end up in the bundle:
156
+
157
+ ```ts
158
+ import { coreRouter } from '@perspectivefi/addresses/prod/keys' // safe addresses only (production)
159
+ import { coreRouter } from '@perspectivefi/addresses/keys' // every status (QA, previews)
160
+
161
+ coreRouter[1] // string | undefined, a frozen { chain -> address } record
162
+ ```
163
+
164
+ One export per registry key, named in camelCase: `core.router` -> `coreRouter`, `external.zodiac.roles-mastercopy-v2.1.0` -> `externalZodiacRolesMastercopyV2_1_0` (a segment that starts with a digit is joined with `_`). Generation fails if two keys would get the same name. In `/prod/keys` a key with no safe chain is an empty frozen object, so imports never break; check the chain you need and hide the feature when it is missing.
165
+
166
+ Each value is emitted as `JSON.parse("{\"$spectraAddresses\":1,\"key\":\"core.router\",\"chains\":{...}}")`. Minifiers never rewrite string literals, so the release check finds these blocks verbatim in the built bundle and checks every `(chain, address)` pair. The package declares `sideEffects: false`.
123
167
 
124
168
  ### Release check (CLI)
125
169
 
126
- `spectra-addresses check` scans a finished build for contract addresses and fails the release if one is not `safe` in the registry, or not in the registry at all.
170
+ `spectra-addresses check` scans a finished build and fails the release when it finds an address that is not approved for the chain it is used on.
127
171
 
128
172
  ```jsonc
129
173
  // package.json of the app
@@ -131,16 +175,19 @@ The package has zero runtime dependencies. The `/prod` bundle contains no non-sa
131
175
  ```
132
176
 
133
177
  ```
134
- spectra-addresses check <paths...> [--mode warn|enforce] [--allow <json file>]... [--registry <url|file>] [--offline]
178
+ spectra-addresses check <paths...> [--mode off|enforce] [--allow <json file>]... [--registry <url|file>] [--offline]
135
179
  ```
136
180
 
137
- - **Mode**: `--mode`, else env `ADDRESS_GATE`, else `warn`. In `warn` it only prints. In `enforce` it exits 1 on violations, but only when `VERCEL_ENV` is unset or `production`; in a Vercel preview build `enforce` is downgraded to `warn`. The effective mode is printed.
138
- - **Latest registry**: the check does not use the registry version your lockfile pins. It fetches `https://cdn.jsdelivr.net/npm/@perspectivefi/addresses@latest/dist/registry.json` (10 second timeout), so an address marked `blocked` today stops today's release even when the app still pins an older package. Override the source with `--registry <url-or-file>` or env `ADDRESS_REGISTRY_URL` (the flag wins). The output says which dataset was used.
139
- - **Fallback**: if the fetch or the JSON shape check fails, the check falls back to the dataset bundled in the installed package and prints a `WARNING` that names the bundled version. This also applies in `enforce` mode: a jsDelivr outage does not fail your build, but the run is only as fresh as the installed version. `--offline` skips the fetch and uses the bundled dataset on purpose.
140
- - Scans files recursively for EVM addresses and Stellar contract ids (skips `*.map`, `cache` and `node_modules` dirs, and binary files over 5 MB).
141
- - Each unique address is checked against every chain: `safe` anywhere is ok; listed but not safe is a violation (shows key, chain, status); not listed is a violation (`unknown`).
142
- - Always allowed: the zero address and `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. `--allow` takes JSON files with an array of address strings (or objects with an `address` field). Use it for token-list addresses.
143
- - Exit codes: `0` ok / warn, `1` violations in effective `enforce`, `2` bad usage.
181
+ - **Mode**: `--mode`, else env `ADDRESS_GATE`. There are two modes. `off` prints the full report and always exits 0. `enforce` exits 1 on any violation. If `VERCEL_ENV=production` and the mode is unset or invalid, the check exits 1 ("ADDRESS_GATE must be set to off or enforce in production"), so a missing setting can never silently disable protection. Outside production an unset or invalid mode means `off`. `enforce` is not downgraded on preview builds.
182
+ - **Tagged blocks first**: every `$spectraAddresses` block (from `/keys`, `/prod/keys`, the runtime datasets and the runtime denylist) is found, also when its JSON is escaped inside a JS string. For each `(chain, address)` in a block, the exact entry for **that chain** must be `safe` in the latest registry and not denylisted. The report names key and chain. A block that cannot be parsed, or carries anything besides the fields we emit, is a violation. The runtime denylist block is accepted only for addresses the registry itself denylists.
183
+ - **Then bare addresses**: the blocks are cut out and the rest of the file is scanned for EVM addresses and Stellar contract ids. A bare address passes only if it is on an `--allow` list, **or** every registry entry with that address (on any chain) is `safe` and it is denylisted on no chain. Otherwise it is a violation, and the report says which chain made it fail (`blocked`, `unsafe`, `denylisted`, or `not in registry`). A denylisted address fails even if it is on an `--allow` list.
184
+ - **Latest registry**: the check does not use the version your lockfile pins. It fetches the latest `dist/registry.json` from jsDelivr (`https://cdn.jsdelivr.net/npm/@perspectivefi/addresses@latest/dist/registry.json`), then from unpkg (`https://unpkg.com/@perspectivefi/addresses@latest/dist/registry.json`), 10 second timeout each. `--registry <url-or-file>` or env `ADDRESS_REGISTRY_URL` replaces both (the flag wins). The output says which dataset was used.
185
+ - **If no source loads**: in `off` the check prints a `WARNING` and uses the dataset bundled in the installed package. In `enforce` it exits 1 ("cannot verify against the latest registry"). `--offline` skips the fetch and uses the bundled dataset on purpose; in `enforce` it prints a loud warning that the run is only as fresh as the installed version.
186
+ - Scans files recursively (skips `*.map`, `cache` and `node_modules` dirs, and binary files over 5 MB).
187
+ - Always allowed: the zero address and `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. `--allow` takes JSON files with an array of address strings (or objects with an `address` field). Use it for token-list addresses. It applies to bare addresses only.
188
+ - Exit codes: `0` ok / off, `1` violations in `enforce`, an unset mode in production, or an unverifiable registry in `enforce`, `2` bad usage.
189
+
190
+ `dist/registry.json` is `{ "entries": [...], "denylist": [...] }` (every field, every status). Versions before 0.2.0 published a bare array; their CLI cannot read the new shape and falls back to its bundled data, so upgrade the CLI together with the first release that uses this one.
144
191
 
145
192
  ### ESLint preset (no hardcoded addresses)
146
193
 
@@ -173,6 +220,26 @@ rules: {
173
220
 
174
221
  (`restrictedSyntax`, `hardcodedAddressRules` and `literalKeyRules` are available as properties of the default export.)
175
222
 
223
+ ## Guarantees
224
+
225
+ What this package guarantees, and what it does not.
226
+
227
+ **Covered**
228
+
229
+ - **Lookups are per chain.** `address`, `statusOf`, `findByAddress`, `isDenied` and the per-key records never mix chains. An address approved on one chain says nothing about the same address on another.
230
+ - **Production build scan.** `spectra-addresses check` runs against the latest published registry (not the pinned one) and fails the release when a built bundle contains an address that is not `safe` on its chain: tagged per-key blocks are checked per chain, and any other address-looking literal (the "bare-address rule") must be `safe` on every chain it is listed on, denylisted nowhere, or be on an `--allow` list. Unknown addresses fail. With the mode set to `enforce` this blocks the release; in production the mode must be set explicitly.
231
+ - **Removed means refused, not forgotten.** Deleting a blocked entry requires moving it to the denylist, and the denylist is in the prod dataset too.
232
+ - **Every `safe` entry has an approval record** (basis and registry PR), and in CI a newly safe entry must be approved by the PR that introduces it.
233
+ - **Nightly on-chain check** that `safe` entries still have their roles with the DAO. It opens an issue; nothing is downgraded automatically.
234
+
235
+ **Not covered here**
236
+
237
+ - **The API runtime gate lives in the consumers.** This package supplies data (`statusOf`, `isDenied`, `inherit`, entries); the API decides what to serve, and how to verify that a pool or vault really comes from an approved factory.
238
+ - **No RPC in consumers.** The package never calls a chain. On-chain checks (bytecode, roles) run only in this repo's CI and nightly job.
239
+ - **App addresses change only with a release.** A frontend bundle contains the addresses it was built with. A status change in the registry reaches an app through its next release, where the build scan checks it against the latest registry; it does not change a bundle that is already deployed. Likewise the API only sees registry changes when it is rebuilt with a newer package.
240
+ - **Addresses assembled at runtime** (concatenated strings, fetched from elsewhere) cannot be found by the scan. Tokens come from `token-list`.
241
+ - **The runtime datasets are chain-aware too.** `/prod` (and the full dataset) is emitted as a tagged block of `(chain, key, address)` entries, so a prod build that uses `address(1, 'core.access-manager')` is judged on chain 1 only, even if the same address is a different, unapproved contract on another chain.
242
+
176
243
  ## Slack announcements
177
244
 
178
245
  After each successful publish, the workflow posts the registry changes to Slack (#nt-deployment-addresses-announcements), most important first:
@@ -196,11 +263,12 @@ npx tsx scripts/announce.ts --from HEAD~1 --to HEAD --dry-run
196
263
 
197
264
  ```
198
265
  npm ci
199
- npm run generate # registry/*.json -> src/generated/registry.ts (commit the result)
266
+ npm run generate # registry/*.json -> src/generated/{registry,keys,keys-prod}.ts (commit the result)
200
267
  npm run validate # static checks on registry/*.json
268
+ npm run validate -- --base origin/main # also compare with the base branch (CI does this on PRs)
201
269
  npm run typecheck
202
270
  npm test # generates, builds, then runs vitest
203
- npm run build # dist/index, dist/prod, dist/cli.js, dist/registry.json
271
+ npm run build # dist/index, dist/prod, dist/keys, dist/prod-keys, dist/cli.js, dist/registry.json
204
272
  npm run onchain [-- --report out.json]
205
273
  ```
206
274