@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 +97 -29
- package/dist/cli.js +327 -70
- package/dist/core-84-fTw11.d.mts +39 -0
- package/dist/core-DGXC_tzy.d.ts +39 -0
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +20 -5
- package/dist/index.mjs +20 -6
- package/dist/keys-runtime-AS8fx_e7.d.ts +6 -0
- package/dist/keys-runtime-CIgeW81X.d.mts +6 -0
- package/dist/keys.d.mts +357 -0
- package/dist/keys.d.ts +357 -0
- package/dist/keys.js +713 -0
- package/dist/keys.mjs +360 -0
- package/dist/prod-keys.d.mts +357 -0
- package/dist/prod-keys.d.ts +357 -0
- package/dist/prod-keys.js +713 -0
- package/dist/prod-keys.mjs +360 -0
- package/dist/prod.d.mts +5 -3
- package/dist/prod.d.ts +5 -3
- package/dist/prod.js +20 -5
- package/dist/prod.mjs +20 -6
- package/dist/{core-CE7Q2MFR.d.mts → registry-rKK0TKIW.d.mts} +1 -26
- package/dist/{core-CE7Q2MFR.d.ts → registry-rKK0TKIW.d.ts} +1 -26
- package/dist/registry.json +19465 -19081
- package/package.json +21 -1
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
|
|
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
|
|
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
|
|
72
|
-
| ------------------------- |
|
|
73
|
-
| `unsafe` -> `role-unsafe` | `source.repo` + 40-hex `source.commit`, `audit` URL
|
|
74
|
-
| `role-unsafe` -> `safe` | Hand admin roles to the DAO, then change status
|
|
75
|
-
| anything -> `blocked` | A reason in `notes`
|
|
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
|
|
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`
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
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
|
|