@gatedpath/preflight 0.1.0
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/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/bip39-words.d.ts +1 -0
- package/dist/bip39-words.js +132 -0
- package/dist/checks/admin.d.ts +24 -0
- package/dist/checks/admin.js +81 -0
- package/dist/checks/balance.d.ts +3 -0
- package/dist/checks/balance.js +39 -0
- package/dist/checks/chain-id.d.ts +2 -0
- package/dist/checks/chain-id.js +11 -0
- package/dist/checks/compiler.d.ts +5 -0
- package/dist/checks/compiler.js +20 -0
- package/dist/checks/deployer.d.ts +14 -0
- package/dist/checks/deployer.js +53 -0
- package/dist/checks/git-secrets.d.ts +14 -0
- package/dist/checks/git-secrets.js +91 -0
- package/dist/checks/slither.d.ts +11 -0
- package/dist/checks/slither.js +119 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +131 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +197 -0
- package/dist/doctor-cli.d.ts +9 -0
- package/dist/doctor-cli.js +76 -0
- package/dist/doctor.d.ts +60 -0
- package/dist/doctor.js +251 -0
- package/dist/dotenv.d.ts +7 -0
- package/dist/dotenv.js +28 -0
- package/dist/gas-cli.d.ts +8 -0
- package/dist/gas-cli.js +100 -0
- package/dist/gas.d.ts +110 -0
- package/dist/gas.js +221 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +20 -0
- package/dist/proc.d.ts +14 -0
- package/dist/proc.js +28 -0
- package/dist/redbelly.d.ts +2 -0
- package/dist/redbelly.js +40 -0
- package/dist/run.d.ts +9 -0
- package/dist/run.js +106 -0
- package/dist/secrets.d.ts +18 -0
- package/dist/secrets.js +89 -0
- package/dist/ship-cli.d.ts +8 -0
- package/dist/ship-cli.js +128 -0
- package/dist/ship.d.ts +108 -0
- package/dist/ship.js +383 -0
- package/dist/suites.d.ts +26 -0
- package/dist/suites.js +53 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +3 -0
- package/dist/well-known.d.ts +2 -0
- package/dist/well-known.js +21 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gatedpath and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @gatedpath/preflight
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
For agents
|
|
5
|
+
- Run `redbelly-preflight --project . --chain 153 --account <keystore-name> --admin <safe>` before
|
|
6
|
+
any deploy and read every line. Exit 1 means do not deploy; fix the failing line, run it again.
|
|
7
|
+
- It never takes a key. --account names a Foundry keystore that cast reads; --address is for a
|
|
8
|
+
read-only run. If you are tempted to pass a hex string, stop.
|
|
9
|
+
- Mainnet (151) refuses an EOA admin and a Safe with threshold below 2. Testnet warns instead.
|
|
10
|
+
- `--json` for CI and for the MCP server. Pins: solc 0.8.30, EVM prague (verified 2026-09-12).
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Seven checks, each printed as `pass`, `fail`, `warn` or `skip` with one sentence saying why,
|
|
14
|
+
then a non-zero exit if anything failed. The package also carries `redbelly-doctor` (the machine
|
|
15
|
+
before the first command) and the `redbelly` bin whose subcommands are `preflight`, `doctor`, `gas`
|
|
16
|
+
(gas per function in RBNT and US cents) and `ship` (the mainnet gate that writes its own report);
|
|
17
|
+
each has a section below. This is the gate PLAN.md sections 5.4 and 6 (Deploy)
|
|
18
|
+
describe, corrected by section 15: there is no network-wide verifier address to match, so the
|
|
19
|
+
deployer's `permission.isAllowed` reading replaces it. Node 18 or newer, TypeScript, one
|
|
20
|
+
runtime dependency (`@gatedpath/chains`, by path). Network traffic is JSON-RPC to the
|
|
21
|
+
one RPC you name and nothing else.
|
|
22
|
+
|
|
23
|
+
## The checks
|
|
24
|
+
|
|
25
|
+
| id | What passes | What fails |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `chain-id` | The chain in `foundry.toml` (`chain_id`), the Hardhat network's `chainId`, or `--chain` equals the RPC's `eth_chainId` | A mismatch, an RPC that is neither 151 nor 153, or an RPC that does not answer. Skips when nothing states an expected chain. |
|
|
28
|
+
| `deployer-verified` | The deployer passes `permission.isAllowed` on the target chain, read through the bootstrap registry | `isAllowed` is false: the wallet has not been through access.redbelly.network. Warns, whatever `isAllowed` says, when the deployer is one of the ten anvil/Hardhat default accounts, whose keys are public and two of which pass `isAllowed` on both networks (RESEARCH.md question 30) |
|
|
29
|
+
| `admin-safe` | `--admin` holds a SafeProxy whose slot 0 is the canonical Safe 1.4.1 singleton or SafeL2 singleton, that singleton's code hashes to the 1.4.1 release, and `getThreshold()` is 2 or more | On 151 an EOA, a non-Safe contract, foreign singleton code or threshold 1 fails; on 153 each of those is a warning |
|
|
30
|
+
| `compiler-pins` | `solc_version` (or `solc`) is 0.8.30 and `evm_version` is prague; Hardhat `solidity.version` and `settings.evmVersion` likewise | Anything else, or no config file |
|
|
31
|
+
| `git-secrets` | No added line anywhere in `git log -p --all` is secret-shaped, and no `.env` is tracked | A bare 64-hex value, a 0x 64-hex value on a line about a key, `PRIVATE_KEY=` with a real value, twelve consecutive BIP-39 words, or a keystore JSON. Skips outside git. |
|
|
32
|
+
| `balance` | Deployer balance covers `--gas` times the latest base fee, priced through the on-chain feed by `gasCostUsd`, plus 25 percent | Short, with the RBNT and USD figures and where to get testnet coins |
|
|
33
|
+
| `slither-report` | `reports/slither.json` or `reports/slither.md` (or `--slither-report`) exists and describes the sources as they are: when a `slither.sources.sha256` sidecar sits beside it, the SHA-256 of every `.sol` under the source directory must equal it; without a sidecar, the report must be newer than every source | Missing, a sidecar that no longer matches, or (with no sidecar) older than a source |
|
|
34
|
+
|
|
35
|
+
"Newer" inside a git repository means the commit time of the file's last change, or the
|
|
36
|
+
working-tree mtime for a modified or untracked file; a checkout gives every file the same
|
|
37
|
+
mtime, so mtimes alone would say nothing. Outside git, mtimes. The time rule has a hole,
|
|
38
|
+
found on 2026-09-12 while running the prompt cards: a clean project's report has no findings,
|
|
39
|
+
so re-running Slither writes byte-identical JSON, git records no change, and the report can
|
|
40
|
+
never become "newer" than a source that was edited after it. Hence the sidecar: the scaffold's
|
|
41
|
+
`npm run lint:slither` writes `reports/slither.sources.sha256` (SHA-256 over every `.sol`
|
|
42
|
+
under `src/`, sorted by POSIX path, each as `path`, newline, bytes, NUL), and when the sidecar
|
|
43
|
+
exists the check compares hashes and ignores times. `hashSources(dir)` is exported for tools
|
|
44
|
+
that write the sidecar.
|
|
45
|
+
|
|
46
|
+
The secrets check streams `git log -p --all --unified=0` and looks only at added lines, so a
|
|
47
|
+
key that was committed and later deleted still fails: it is in the history and history is
|
|
48
|
+
what gets cloned. Findings name the file, the commit and the kind of match, never the text.
|
|
49
|
+
Lockfiles, `lib/`, `out/`, `node_modules/` and minified files are skipped; lines that mention
|
|
50
|
+
sha256, keccak, integrity, a tx hash or a JSON `"id"` are treated as hashes.
|
|
51
|
+
|
|
52
|
+
## redbelly-doctor
|
|
53
|
+
|
|
54
|
+
The machine before the first command (PLAN.md 18.3). `redbelly-doctor` (also `redbelly doctor`,
|
|
55
|
+
`npm run doctor` in a scaffold, and the MCP server's `doctor` tool) prints one line per check with
|
|
56
|
+
`pass`, `warn` or `fail` and the fix on the same line, and exits 0 only when every required check
|
|
57
|
+
passes; `--json` for agents. The fix text is the failure table's (`@gatedpath/agent-rules`), so
|
|
58
|
+
`explain_failure` says the same thing about a pasted doctor line.
|
|
59
|
+
|
|
60
|
+
| id | required | What it checks |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `node` | yes | Node 22 or later |
|
|
63
|
+
| `git` | yes | git on PATH |
|
|
64
|
+
| `forge`, `anvil`, `cast` | yes | each on PATH and answering `--version` |
|
|
65
|
+
| `foundry-version` | yes | the three report one version |
|
|
66
|
+
| `foundry-exit-code` | yes | each hands its exit code back. The `@foundry-rs` npm packages (1.7.1) install a Node shim that exits 0 whatever the binary returned, so a failed `forge build` reports success; doctor probes with a flag no binary knows (the real ones exit 2, the shim 0), names the shim, and prints the `ln -sf` line to the real binary it found in the platform package beside it |
|
|
67
|
+
| `slither`, `aderyn` | no | on PATH; a warning when missing or not at the version CI pins (0.11.6, 0.6.8) |
|
|
68
|
+
| `env-tracked` | yes | no `.env` or `.env.*` (other than `.env.example`) in `git ls-files` |
|
|
69
|
+
| `vendor` | yes | in a scaffold, each `vendor/redbelly-*/package.json` version equals the record the scaffolder wrote under `redbelly.vendor` in the root `package.json`; a scaffold without the record warns |
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
redbelly-doctor [--project <dir>] [--json]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## redbelly gas
|
|
76
|
+
|
|
77
|
+
A gas report in the unit the chain charges in (PLAN.md 18.3). `redbelly gas` (also `npm run gas` in a
|
|
78
|
+
scaffold and the MCP server's `gas_report` tool) reads `.gas-snapshot` when the project has one (one
|
|
79
|
+
row per test; fuzz lines by their mean) or runs `forge test --gas-report --json` (the deployment and
|
|
80
|
+
every function of the contracts in `gas_reports`), prices each row at the latest base fee and the
|
|
81
|
+
on-chain feed price with the same integer arithmetic as `gasCostUsd`, and prints gas, RBNT and US
|
|
82
|
+
cents per row. `--diff <file>` compares against a previous snapshot or report, either format, and
|
|
83
|
+
adds the gas and cents difference and what was added or gone. Two RPC reads, nothing else.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
redbelly gas [--project <dir>] [--chain 151|153] [--rpc <url>] [--snapshot <file>] [--report] [--diff <file>] [--json]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Recorded runs against the real feeds, read-only, on 14 September 2026: `reports/gas-153-2026-09-14.md`
|
|
90
|
+
and `reports/gas-151-2026-09-14.md`. On both, 21,000 gas came to 1.000 US cents and the scaffold's
|
|
91
|
+
`GatedERC20` deploys for 1,898,391 gas, 383.7 RBNT, 90.4 cents at a feed price of US$0.002356 per RBNT.
|
|
92
|
+
|
|
93
|
+
## redbelly ship
|
|
94
|
+
|
|
95
|
+
The mainnet gate that produces the document you wanted anyway (PLAN.md 18.3). `redbelly ship --chain
|
|
96
|
+
<151|153> --account <keystore-name> [--admin <safe>] [--gas <n>]` (also `npm run ship` in a scaffold
|
|
97
|
+
and the MCP server's `ship_report` tool) runs the gas report first (its deployment row is the gas the
|
|
98
|
+
balance check prices), then pre-flight's seven checks with the keystore's address, checks the verifier
|
|
99
|
+
(`--verifier` or `VERIFIER` in `.env`; required on 151, where a missing one fails and on 153 warns that
|
|
100
|
+
a `ReceptorMock` will be deployed), runs `forge test --json` and counts the suites that inherit
|
|
101
|
+
`GatedTest`, reads the admin (Safe version, singleton, threshold, owners, `isAllowed`) and writes
|
|
102
|
+
`deployments/ship-<chain>-<date>.md`: a header line `status` reads back, every check with its result,
|
|
103
|
+
the deployer and admin readings, the contracts from `deployments/<chain>-*.json` with their
|
|
104
|
+
constructor arguments and the exact `forge verify-contract` command, the gas table, the tests, the
|
|
105
|
+
Slither line, the compiler pins, and the list of what the report does not prove. It refuses to write
|
|
106
|
+
when any check fails and exits 1 with the failing lines. Run it again after the deploy to fill in the
|
|
107
|
+
contract section; the file is dated in UTC.
|
|
108
|
+
|
|
109
|
+
The scaffold's `RedbellyDeployScript.sol` refuses to broadcast on 151 without `deployments/ship-151-<today>.md`
|
|
110
|
+
(a dry run says so and continues; 153 warns), and the Hardhat deploy script does the same. Tested on
|
|
111
|
+
a local chain 151 with the mocked registry, the real Safe 1.4.1 bytecode and a keystore `cast wallet
|
|
112
|
+
new` created for the test; nothing signs against a real network.
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
redbelly-preflight [--project <dir>] [--profile <name>] [--chain 151|153] [--rpc <url>]
|
|
118
|
+
[--account <keystore-name> [--password-file <path>] | --address <0x…>]
|
|
119
|
+
[--admin <0x…>] [--gas <units>] [--slither-report <path>]
|
|
120
|
+
[--skip <id,id>] [--json]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--account` runs `cast wallet address --account <name>` in the project directory; cast
|
|
124
|
+
decrypts the keystore, this process sees the address. Foundry looks for the keystore under
|
|
125
|
+
`$HOME/.foundry/keystores`. Without a terminal, give cast the password with
|
|
126
|
+
`--password-file` or `ETH_PASSWORD`. `--address` does the same checks read-only.
|
|
127
|
+
|
|
128
|
+
Flags you leave out are filled from the project's `.env` (parsed, never executed) or the
|
|
129
|
+
environment, using the names the scaffolder's templates already use: `DEPLOYER` for
|
|
130
|
+
`--address`, `ADMIN_SAFE` for `--admin`, `CHAIN_ID` for `--chain` when the config names no
|
|
131
|
+
chain. So inside a `create-redbelly-dapp` project the whole call is `redbelly-preflight`.
|
|
132
|
+
`--gas` defaults to 3,000,000; pass what `forge script` estimated. With `--json` the whole
|
|
133
|
+
report (below) goes to stdout.
|
|
134
|
+
|
|
135
|
+
This is the same set of checks the scaffolder's `RedbellyDeployScript.preflight()` makes in
|
|
136
|
+
Solidity at broadcast time and its `scripts/preflight.mjs` makes offline, generalised so any
|
|
137
|
+
Foundry or Hardhat project can run them, with the history scan, the compiler pins, the
|
|
138
|
+
balance margin and the Slither freshness added.
|
|
139
|
+
|
|
140
|
+
Exit codes: 0 no check failed (warnings allowed), 1 at least one fail, 2 bad arguments. An
|
|
141
|
+
argument that looks like a private key is a bad argument.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { runPreflight, renderText } from '@gatedpath/preflight';
|
|
145
|
+
const report = await runPreflight({ project: '.', chain: 153, address: '0x…', admin: '0x…', gas: 1_200_000 });
|
|
146
|
+
process.stdout.write(renderText(report));
|
|
147
|
+
if (!report.ok) process.exit(1);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The report: `{ tool, version, generatedAt, project, config: { kind, file, profile }, chain:
|
|
151
|
+
{ expected, reported, rpc, network }, deployer, admin, checks: [{ id, status, reason, data }],
|
|
152
|
+
ok }`. `data` carries the numbers (threshold, balances, feed price, findings); it never
|
|
153
|
+
carries a secret.
|
|
154
|
+
|
|
155
|
+
## Tests
|
|
156
|
+
|
|
157
|
+
`npm test` builds, then runs `test/unit.test.mjs` (TOML and Hardhat readers, the secret
|
|
158
|
+
scanner, the Safe fixture hashes) and `test/preflight.test.mjs`, which spawns `anvil`
|
|
159
|
+
twice, on chain 151 and on 153, and:
|
|
160
|
+
|
|
161
|
+
- etches hand-assembled bytecode for the bootstrap registry, the permission contract and the
|
|
162
|
+
price feed at the addresses `@gatedpath/chains` records (`test/mocks.mjs`), so the
|
|
163
|
+
real helpers run their real code paths;
|
|
164
|
+
- etches the genuine Safe 1.4.1 runtime code, read from testnet with `eth_getCode` on
|
|
165
|
+
2026-09-12 and byte-identical on mainnet (`test/fixtures/safe-1.4.1/`), at the canonical
|
|
166
|
+
addresses, and deploys proxies through the real `SafeProxyFactory.createProxyWithNonce`
|
|
167
|
+
from an unlocked anvil account, so `getThreshold()` and the singleton reads are answered by
|
|
168
|
+
Safe's own logic. Etching, not compiling: no Solidity toolchain in the test and the bytes
|
|
169
|
+
are the ones on chain;
|
|
170
|
+
- builds temporary git repositories with pinned commit dates for the secrets and Slither
|
|
171
|
+
checks;
|
|
172
|
+
- creates a throwaway keystore with `cast wallet new` for the `--account` path (the key is
|
|
173
|
+
generated inside cast, encrypted at once and never printed);
|
|
174
|
+
- checks the hardcoded list of anvil default accounts against the spawned anvil's `eth_accounts`.
|
|
175
|
+
|
|
176
|
+
29 tests. `npm run test:live` runs the read-only checks against the real testnet RPC using
|
|
177
|
+
the known allowed address from `@gatedpath/chains` as both deployer and admin.
|
|
178
|
+
|
|
179
|
+
## Last verified
|
|
180
|
+
|
|
181
|
+
2026-09-12. `npm test`: 29 passed (8 unit, 21 against anvil on 151 and 153). `npm run
|
|
182
|
+
test:live`: 1 passed. The live run's output is committed under `reports/` as
|
|
183
|
+
`testnet-153-2026-09-12.txt` and `.json`: chain, deployer, compiler, secrets, balance and
|
|
184
|
+
Slither pass; the admin warns because the known allowed address is an EOA, which is exactly
|
|
185
|
+
what the check is for. 1,000,000 gas cost 195.96 RBNT, US$0.4762 at the feed's US$0.002431,
|
|
186
|
+
which is the fixed-USD gas model in one line.
|
|
187
|
+
|
|
188
|
+
Anvil's base fee is not Redbelly's, so the balance figures in the anvil tests only prove
|
|
189
|
+
the arithmetic and the margin; the live run proves the price.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BIP39_ENGLISH: ReadonlySet<string>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// BIP-39 English wordlist (2048 words), fetched from github.com/bitcoin/bips/blob/master/bip-0039/english.txt
|
|
2
|
+
// on 2026-09-12. Used only to spot a seed phrase pasted into a file; nothing here generates keys.
|
|
3
|
+
export const BIP39_ENGLISH = new Set([
|
|
4
|
+
"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid",
|
|
5
|
+
"acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance",
|
|
6
|
+
"advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
|
|
7
|
+
"alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among",
|
|
8
|
+
"amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique",
|
|
9
|
+
"anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor",
|
|
10
|
+
"army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume",
|
|
11
|
+
"asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado",
|
|
12
|
+
"avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball",
|
|
13
|
+
"bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become",
|
|
14
|
+
"beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle",
|
|
15
|
+
"bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood",
|
|
16
|
+
"blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring",
|
|
17
|
+
"borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief",
|
|
18
|
+
"bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb",
|
|
19
|
+
"bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable",
|
|
20
|
+
"cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable",
|
|
21
|
+
"capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog",
|
|
22
|
+
"catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk",
|
|
23
|
+
"champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child",
|
|
24
|
+
"chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify",
|
|
25
|
+
"claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud",
|
|
26
|
+
"clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine",
|
|
27
|
+
"come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper",
|
|
28
|
+
"copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle",
|
|
29
|
+
"craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop",
|
|
30
|
+
"cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious",
|
|
31
|
+
"current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn",
|
|
32
|
+
"day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay",
|
|
33
|
+
"deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk",
|
|
34
|
+
"despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital",
|
|
35
|
+
"dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide",
|
|
36
|
+
"divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft",
|
|
37
|
+
"dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb",
|
|
38
|
+
"dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo",
|
|
39
|
+
"ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator",
|
|
40
|
+
"elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy",
|
|
41
|
+
"energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode",
|
|
42
|
+
"equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil",
|
|
43
|
+
"evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit",
|
|
44
|
+
"exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint",
|
|
45
|
+
"faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault",
|
|
46
|
+
"favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field",
|
|
47
|
+
"figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness",
|
|
48
|
+
"fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly",
|
|
49
|
+
"foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil",
|
|
50
|
+
"foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel",
|
|
51
|
+
"fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment",
|
|
52
|
+
"gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle",
|
|
53
|
+
"ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue",
|
|
54
|
+
"goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass",
|
|
55
|
+
"gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun",
|
|
56
|
+
"gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard",
|
|
57
|
+
"head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip",
|
|
58
|
+
"hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital",
|
|
59
|
+
"host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband",
|
|
60
|
+
"hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose",
|
|
61
|
+
"improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial",
|
|
62
|
+
"inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest",
|
|
63
|
+
"invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel",
|
|
64
|
+
"job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup",
|
|
65
|
+
"key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know",
|
|
66
|
+
"lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law",
|
|
67
|
+
"lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend",
|
|
68
|
+
"length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit",
|
|
69
|
+
"link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop",
|
|
70
|
+
"lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet",
|
|
71
|
+
"maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin",
|
|
72
|
+
"marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure",
|
|
73
|
+
"meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message",
|
|
74
|
+
"metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake",
|
|
75
|
+
"mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning",
|
|
76
|
+
"mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music",
|
|
77
|
+
"must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative",
|
|
78
|
+
"neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee",
|
|
79
|
+
"noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey",
|
|
80
|
+
"object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay",
|
|
81
|
+
"old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit",
|
|
82
|
+
"orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over",
|
|
83
|
+
"own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper",
|
|
84
|
+
"parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut",
|
|
85
|
+
"pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical",
|
|
86
|
+
"piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet",
|
|
87
|
+
"plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony",
|
|
88
|
+
"pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare",
|
|
89
|
+
"present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program",
|
|
90
|
+
"project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil",
|
|
91
|
+
"puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz",
|
|
92
|
+
"quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid",
|
|
93
|
+
"rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle",
|
|
94
|
+
"reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove",
|
|
95
|
+
"render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire",
|
|
96
|
+
"retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid",
|
|
97
|
+
"ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room",
|
|
98
|
+
"rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness",
|
|
99
|
+
"safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say",
|
|
100
|
+
"scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea",
|
|
101
|
+
"search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence",
|
|
102
|
+
"series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine",
|
|
103
|
+
"ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side",
|
|
104
|
+
"siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size",
|
|
105
|
+
"skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan",
|
|
106
|
+
"slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social",
|
|
107
|
+
"sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup",
|
|
108
|
+
"source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin",
|
|
109
|
+
"spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium",
|
|
110
|
+
"staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting",
|
|
111
|
+
"stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject",
|
|
112
|
+
"submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme",
|
|
113
|
+
"sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim",
|
|
114
|
+
"swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target",
|
|
115
|
+
"task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that",
|
|
116
|
+
"theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger",
|
|
117
|
+
"tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token",
|
|
118
|
+
"tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist",
|
|
119
|
+
"toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree",
|
|
120
|
+
"trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try",
|
|
121
|
+
"tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical",
|
|
122
|
+
"ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown",
|
|
123
|
+
"unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful",
|
|
124
|
+
"useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle",
|
|
125
|
+
"velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view",
|
|
126
|
+
"village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote",
|
|
127
|
+
"voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave",
|
|
128
|
+
"way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat",
|
|
129
|
+
"wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner",
|
|
130
|
+
"winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth",
|
|
131
|
+
"wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo",
|
|
132
|
+
]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* keccak256 of the runtime code at the canonical Safe 1.4.1 addresses, read with eth_getCode
|
|
4
|
+
* from both governors RPCs on 2026-09-12 (identical on 151 and 153). A singleton whose code
|
|
5
|
+
* hashes to something else is not the Safe 1.4.1 release, whatever address it sits at.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SAFE_141_CODE_HASHES: {
|
|
8
|
+
readonly safeSingleton: "0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4";
|
|
9
|
+
readonly safeL2Singleton: "0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff";
|
|
10
|
+
readonly safeProxyFactory: "0x50c3cdc4074750a7a974204a716c999edd37482f907608d960b2b025ee0b3317";
|
|
11
|
+
readonly safeFallbackHandler: "0x7c6007a5d711cea8dfd5d91f5940ec29c7f200fe511eb1fc1397b367af3c42f9";
|
|
12
|
+
};
|
|
13
|
+
/** keccak256 of runtime code given as a 0x hex string, hashing the bytes rather than the text. */
|
|
14
|
+
export declare function codeHash(codeHex: string): string;
|
|
15
|
+
export interface AdminReading {
|
|
16
|
+
address: string;
|
|
17
|
+
isContract: boolean;
|
|
18
|
+
singleton: string | null;
|
|
19
|
+
singletonKind: 'safeSingleton' | 'safeL2Singleton' | null;
|
|
20
|
+
singletonCodeMatches: boolean | null;
|
|
21
|
+
threshold: number | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function readAdmin(address: string, rpc: string, network: 'mainnet' | 'testnet'): Promise<AdminReading>;
|
|
24
|
+
export declare function checkAdminSafe(admin: string | null, rpc: string, chain: number | null): Promise<CheckResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Is the admin a Safe? On mainnet the admin address must hold a SafeProxy whose singleton is
|
|
2
|
+
// the canonical Safe 1.4.1 (L1 or L2 singleton), the singleton must carry the 1.4.1 bytecode,
|
|
3
|
+
// and getThreshold() must be at least 2. On testnet the same reads run but an EOA or a low
|
|
4
|
+
// threshold is a warning, because testnet is for trying things.
|
|
5
|
+
import { addresses, ethCall, keccak256Hex, toCaller } from '@gatedpath/chains';
|
|
6
|
+
/**
|
|
7
|
+
* keccak256 of the runtime code at the canonical Safe 1.4.1 addresses, read with eth_getCode
|
|
8
|
+
* from both governors RPCs on 2026-09-12 (identical on 151 and 153). A singleton whose code
|
|
9
|
+
* hashes to something else is not the Safe 1.4.1 release, whatever address it sits at.
|
|
10
|
+
*/
|
|
11
|
+
export const SAFE_141_CODE_HASHES = {
|
|
12
|
+
safeSingleton: '0x1fe2df852ba3299d6534ef416eefa406e56ced995bca886ab7a553e6d0c5e1c4',
|
|
13
|
+
safeL2Singleton: '0xb1f926978a0f44a2c0ec8fe822418ae969bd8c3f18d61e5103100339894f81ff',
|
|
14
|
+
safeProxyFactory: '0x50c3cdc4074750a7a974204a716c999edd37482f907608d960b2b025ee0b3317',
|
|
15
|
+
safeFallbackHandler: '0x7c6007a5d711cea8dfd5d91f5940ec29c7f200fe511eb1fc1397b367af3c42f9',
|
|
16
|
+
};
|
|
17
|
+
const GET_THRESHOLD = '0xe75235b8'; // getThreshold()
|
|
18
|
+
/** keccak256 of runtime code given as a 0x hex string, hashing the bytes rather than the text. */
|
|
19
|
+
export function codeHash(codeHex) {
|
|
20
|
+
return keccak256Hex(Buffer.from(codeHex.replace(/^0x/, ''), 'hex'));
|
|
21
|
+
}
|
|
22
|
+
export async function readAdmin(address, rpc, network) {
|
|
23
|
+
const caller = toCaller(rpc);
|
|
24
|
+
const code = (await caller('eth_getCode', [address, 'latest']));
|
|
25
|
+
const reading = { address, isContract: code !== '0x' && code !== '0x0' && code.length > 2, singleton: null, singletonKind: null, singletonCodeMatches: null, threshold: null };
|
|
26
|
+
if (!reading.isContract)
|
|
27
|
+
return reading;
|
|
28
|
+
const slot0 = (await caller('eth_getStorageAt', [address, '0x0', 'latest']));
|
|
29
|
+
const singleton = '0x' + slot0.slice(-40);
|
|
30
|
+
reading.singleton = singleton;
|
|
31
|
+
const known = addresses[network];
|
|
32
|
+
for (const kind of ['safeSingleton', 'safeL2Singleton']) {
|
|
33
|
+
if (known[kind].address.toLowerCase() === singleton.toLowerCase())
|
|
34
|
+
reading.singletonKind = kind;
|
|
35
|
+
}
|
|
36
|
+
if (reading.singletonKind) {
|
|
37
|
+
const singletonCode = (await caller('eth_getCode', [singleton, 'latest']));
|
|
38
|
+
reading.singletonCodeMatches = codeHash(singletonCode) === SAFE_141_CODE_HASHES[reading.singletonKind];
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const ret = await ethCall(rpc, address, GET_THRESHOLD);
|
|
42
|
+
if (ret && ret !== '0x')
|
|
43
|
+
reading.threshold = Number(BigInt(ret));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
reading.threshold = null;
|
|
47
|
+
}
|
|
48
|
+
return reading;
|
|
49
|
+
}
|
|
50
|
+
export async function checkAdminSafe(admin, rpc, chain) {
|
|
51
|
+
if (!admin)
|
|
52
|
+
return { id: 'admin-safe', status: 'skip', reason: 'No admin given; pass --admin <0x…> with the address that will own the deployed contracts.' };
|
|
53
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(admin))
|
|
54
|
+
return { id: 'admin-safe', status: 'fail', reason: `"${admin}" is not a 20-byte hex address.` };
|
|
55
|
+
if (chain !== 151 && chain !== 153)
|
|
56
|
+
return { id: 'admin-safe', status: 'skip', reason: 'The RPC is not a Redbelly chain.' };
|
|
57
|
+
const network = chain === 151 ? 'mainnet' : 'testnet';
|
|
58
|
+
const bad = chain === 151 ? 'fail' : 'warn';
|
|
59
|
+
let r;
|
|
60
|
+
try {
|
|
61
|
+
r = await readAdmin(admin, rpc, network);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
return { id: 'admin-safe', status: 'fail', reason: `Could not read the admin address: ${e.message}.` };
|
|
65
|
+
}
|
|
66
|
+
const data = { ...r };
|
|
67
|
+
if (!r.isContract) {
|
|
68
|
+
return chain === 151
|
|
69
|
+
? { id: 'admin-safe', status: 'fail', reason: `Admin ${admin} is an externally owned account; on mainnet the admin must be a Safe 1.4.1 with a threshold of at least 2.`, data }
|
|
70
|
+
: { id: 'admin-safe', status: 'warn', reason: `Admin ${admin} is an externally owned account; fine on testnet, but mainnet pre-flight will refuse it, so put a Safe in place now.`, data };
|
|
71
|
+
}
|
|
72
|
+
if (!r.singletonKind)
|
|
73
|
+
return { id: 'admin-safe', status: bad, reason: `Admin ${admin} is a contract but its storage slot 0 (${r.singleton}) is not the canonical Safe 1.4.1 singleton or SafeL2 singleton on this chain.`, data };
|
|
74
|
+
if (r.singletonCodeMatches === false)
|
|
75
|
+
return { id: 'admin-safe', status: bad, reason: `Admin ${admin} points at ${r.singleton}, whose code does not hash to the Safe 1.4.1 release.`, data };
|
|
76
|
+
if (r.threshold === null)
|
|
77
|
+
return { id: 'admin-safe', status: bad, reason: `Admin ${admin} looks like a Safe but getThreshold() did not answer; the proxy may not be set up.`, data };
|
|
78
|
+
if (r.threshold < 2)
|
|
79
|
+
return { id: 'admin-safe', status: bad, reason: `Admin ${admin} is a Safe 1.4.1 with threshold ${r.threshold}; one signer is one stolen key away from your contracts, so use 2 or more.`, data };
|
|
80
|
+
return { id: 'admin-safe', status: 'pass', reason: `Admin ${admin} is a Safe 1.4.1 (${r.singletonKind === 'safeL2Singleton' ? 'SafeL2' : 'Safe'} singleton, code hash verified) with threshold ${r.threshold}.`, data };
|
|
81
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Does the deployer hold enough RBNT? Cost is gas times the latest base fee, priced in USD
|
|
2
|
+
// through the on-chain feed by gasCostUsd from @gatedpath/chains, with 25 percent on
|
|
3
|
+
// top because the base fee moves with the RBNT price between now and the send.
|
|
4
|
+
import { formatUnits, gasCostUsd, toCaller } from '@gatedpath/chains';
|
|
5
|
+
export const MARGIN_PERCENT = 25;
|
|
6
|
+
export async function checkBalance(deployer, gas, rpc, chain) {
|
|
7
|
+
if (!deployer)
|
|
8
|
+
return { id: 'balance', status: 'skip', reason: 'No deployer given, so there is no balance to read.' };
|
|
9
|
+
if (chain !== 151 && chain !== 153)
|
|
10
|
+
return { id: 'balance', status: 'skip', reason: 'The RPC is not a Redbelly chain, so the price feed is not there to read.' };
|
|
11
|
+
try {
|
|
12
|
+
const caller = toCaller(rpc);
|
|
13
|
+
const balance = BigInt((await caller('eth_getBalance', [deployer, 'latest'])));
|
|
14
|
+
const cost = await gasCostUsd({ gasUsed: gas, rpc });
|
|
15
|
+
const required = (cost.wei * BigInt(100 + MARGIN_PERCENT)) / 100n;
|
|
16
|
+
const data = {
|
|
17
|
+
deployer,
|
|
18
|
+
gas: gas.toString(),
|
|
19
|
+
gasPriceWei: cost.gasPriceWei.toString(),
|
|
20
|
+
costWei: cost.wei.toString(),
|
|
21
|
+
costRbnt: cost.rbnt,
|
|
22
|
+
costUsd: cost.usd,
|
|
23
|
+
usdPerRbnt: cost.usdPerRbnt,
|
|
24
|
+
requiredWei: required.toString(),
|
|
25
|
+
requiredRbnt: formatUnits(required, 18),
|
|
26
|
+
balanceWei: balance.toString(),
|
|
27
|
+
balanceRbnt: formatUnits(balance, 18),
|
|
28
|
+
marginPercent: MARGIN_PERCENT,
|
|
29
|
+
};
|
|
30
|
+
const short = (s) => (s.includes('.') ? s.replace(/(\.\d{4})\d+$/, '$1') : s);
|
|
31
|
+
if (balance >= required) {
|
|
32
|
+
return { id: 'balance', status: 'pass', reason: `${deployer} holds ${short(data.balanceRbnt)} RBNT; ${gas.toLocaleString('en-US')} gas costs about ${short(cost.rbnt)} RBNT (US$${cost.usd.toFixed(4)}) and ${short(data.requiredRbnt)} RBNT covers it with ${MARGIN_PERCENT}% margin.`, data };
|
|
33
|
+
}
|
|
34
|
+
return { id: 'balance', status: 'fail', reason: `${deployer} holds ${short(data.balanceRbnt)} RBNT but ${gas.toLocaleString('en-US')} gas needs ${short(data.requiredRbnt)} RBNT with ${MARGIN_PERCENT}% margin (US$${cost.usd.toFixed(4)} at the feed price); ${chain === 153 ? 'get testnet RBNT at https://redbelly.faucetme.pro/' : 'fund the deployer first'}.`, data };
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return { id: 'balance', status: 'fail', reason: `Could not price the deployment: ${e.message}.`, data: { deployer, gas: gas.toString() } };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function checkChainId(expected, reported, expectedSource, rpc) {
|
|
2
|
+
if (reported === null)
|
|
3
|
+
return { id: 'chain-id', status: 'fail', reason: `The RPC at ${rpc} did not answer eth_chainId.` };
|
|
4
|
+
if (reported !== 151 && reported !== 153)
|
|
5
|
+
return { id: 'chain-id', status: 'fail', reason: `The RPC reports chain ${reported}, which is neither Redbelly mainnet (151) nor testnet (153).`, data: { expected, reported } };
|
|
6
|
+
if (expected === null)
|
|
7
|
+
return { id: 'chain-id', status: 'skip', reason: `No chain ID in the project config; pass --chain to compare it with the RPC's ${reported}.`, data: { expected, reported } };
|
|
8
|
+
if (expected !== reported)
|
|
9
|
+
return { id: 'chain-id', status: 'fail', reason: `${expectedSource} says chain ${expected} but the RPC at ${rpc} reports ${reported}.`, data: { expected, reported } };
|
|
10
|
+
return { id: 'chain-id', status: 'pass', reason: `${expectedSource} says chain ${expected} and the RPC reports ${reported}.`, data: { expected, reported } };
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SOLC_PIN = '0.8.30';
|
|
2
|
+
export const EVM_PIN = 'prague';
|
|
3
|
+
export function checkCompilerPins(config) {
|
|
4
|
+
if (config.kind === 'none')
|
|
5
|
+
return { id: 'compiler-pins', status: 'fail', reason: 'No foundry.toml or hardhat.config.* found, so the compiler and EVM pins cannot be confirmed.' };
|
|
6
|
+
const where = `${config.file}${config.profile ? ` (${config.kind === 'foundry' ? 'profile' : 'network'} ${config.profile})` : ''}`;
|
|
7
|
+
const data = { solc: config.solc, evmVersion: config.evmVersion, file: config.file, profile: config.profile };
|
|
8
|
+
const problems = [];
|
|
9
|
+
if (config.solc === null)
|
|
10
|
+
problems.push(`no ${config.kind === 'foundry' ? 'solc_version' : 'solidity version'} is pinned`);
|
|
11
|
+
else if (config.solc !== SOLC_PIN)
|
|
12
|
+
problems.push(`solc is ${config.solc}, not ${SOLC_PIN}`);
|
|
13
|
+
if (config.evmVersion === null)
|
|
14
|
+
problems.push(`no ${config.kind === 'foundry' ? 'evm_version' : 'evmVersion'} is set`);
|
|
15
|
+
else if (config.evmVersion.toLowerCase() !== EVM_PIN)
|
|
16
|
+
problems.push(`evm version is ${config.evmVersion}, not ${EVM_PIN}`);
|
|
17
|
+
if (problems.length > 0)
|
|
18
|
+
return { id: 'compiler-pins', status: 'fail', reason: `In ${where}, ${problems.join(' and ')}; Redbelly is Prague with solc ${SOLC_PIN} on both networks (verified 2026-09-12).`, data };
|
|
19
|
+
return { id: 'compiler-pins', status: 'pass', reason: `${where} pins solc ${SOLC_PIN} and EVM ${EVM_PIN}.`, data };
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
export interface DeployerSource {
|
|
3
|
+
account?: string | undefined;
|
|
4
|
+
address?: string | undefined;
|
|
5
|
+
passwordFile?: string | undefined;
|
|
6
|
+
cwd: string;
|
|
7
|
+
env: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveDeployer(src: DeployerSource): Promise<{
|
|
10
|
+
address: string | null;
|
|
11
|
+
how: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function checkDeployerVerified(address: string | null, how: string, error: string | undefined, rpc: string, chain: number | null): Promise<CheckResult>;
|