refactor(script): merge the per-chain token-copy scripts into one - #291
Conversation
…ning, post-swap test retirement Rebased onto the #254 grant-map restructure (13-entry parametric form, STOX_PROD_AUTHORISER repointed to the live authoriser upstream). GRANTEE_SERVICE_3D0C joins GRANTEE_SERVICE_1C66 as an ADDITIONAL service signer with the same three action roles: - expectedGrants() 13 -> 16; every consumer (parity authoriser leg, production-state bundle, per-chain prod pins) is red per chain until the provisioning bundle executes there, drift-guarded after. 3D0C joins the DEFAULT_ADMIN absence checks. Map docs state purpose, not contents. - 20260619 clone-deploy MIRROR_COUNT 6 -> 9 (+ suite): the ceremony runs again on future chains and its slice guard forces the constant to track the canonical map; executed chains refuse re-runs via hydrated pins. - 20260723-provision-additional-service-signer (run-script, registered): authors the per-chain grant bundle, work items derived from the map, chain-aware (base/ethereum network selector added to the dispatcher), self-scoping, n+1-proven reversible. Fork suite green on live Base + Ethereum. - Post-execution retirement: the executed swap authorings' one-shot test suites deleted; live-state coverage is carried by the standing invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
- LibSafeInvariants.HYPEREVM_CHAIN_ID = 999. - BuildPointers emits STOX_PROD_AUTHORISER_V4_CLONE_HYPEREVM = address(0) into the generated lib — the unhydrated pin, symmetric with the Ethereum clone pin's pre-deploy state. - 20260619 clone-deploy script's activeChainClonePin() gains the HyperEVM branch, so the same broadcast (already dispatchable on `hyperevm` via manual-broadcast) deploys + configures the HyperEVM authoriser once its upstream gates land, and refuses re-deploys after the pin hydrates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
The 20260619 ceremony executed on HyperEVM 2026-07-24 (manual-broadcast run 30101296528): authoriser deployed at 0x66566cc91dEAf818859bD4b09B7903ac48998157 — the same address as Ethereum's (same CloneFactory, same first-nonce CREATE) — with the shared EIP-1167 codehash, the full 16-entry grant map (both service signers, verified live) landed on the HyperEVM Safe + service EOAs, and the deployer renounced. Hydrate the pin from the run's logged address. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
- STOX_TOKEN_OWNER_SAFE_HYPEREVM = address(0) until the freshly-created HyperEVM Safe's address lands (canonical Safe v1.4.1 infra verified live on HyperEVM at the canonical addresses, 2026-07-22). - safeForChainId branch for chain 999 — every consumer of assertActiveChainTokenOwnerSafe becomes HyperEVM-aware the moment the pin hydrates, with zero further code changes. - HyperEvmTokenOwnerSafeParityTest, mirroring the Ethereum parity pin: PENDING (loud) while the pin is unhydrated, then asserts the shared policy against the live HyperEVM Safe. Second loud PENDING gate on the RPC env: the shared rainix test workflow has no HyperEVM secret slot yet, so CI cannot fork HyperEVM until rainix grows one (noted for follow-up in RAI-1511). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
…alisation (RAI-1511) - Rename LibProdBeaconsEthereum -> LibProdBeacons0_1_1: the set is Zoltu-deterministic, so it is the SAME addresses on every chain that bootstraps at 0.1.1 — Ethereum yesterday, HyperEVM next. Consumers updated; docs record the rename rationale. - prodBeaconsForChainId: HyperEVM branch (resolves the same 0.1.1 set). - 20260722-migrate-beacon-owners-hyperevm: the HyperEVM leg of the beacon-ownership migration executed for Base (#253) and Ethereum (20260716). Deploy-EOA broadcast (beacons come up EOA-owned), --legacy noted for HyperEVM's fee-estimation quirk, chain-id guarded, PENDING. Ordering per RAI-1511: after impl suites + Safe pin, before tokens (the token-deploy pre-flight hard-gates on beacon ownership). - HyperEvmBeaconOwnershipTest: the migration's forcing function, mirroring the Ethereum pin, with loud PENDING gates on the Safe pin + the missing rainix HyperEVM RPC secret slot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
- productionTokensHyperEvm(): 29 all-zero rows in Base row order — the explicit "missing on this chain" state the gap-filling deploy targets and the parity pin flags until hydrated from the executed deploy. - Cross-chain parity: HyperEVM third leg + full HyperEVM-vs-Base comparisons, gated on RPC availability (the shared rainix test workflow has no HyperEVM secret slot yet — loudly pending, and the new HYPEREVM_PARITY_DEADLINE (2026-11-01) turns a still-missing RPC or still-pending leg into a failure rather than letting the gap live forever). - StoxProdV4.testProdDeployHyperEvmV4: the impl-deploy + beacon-migration forcing function, env-gated the same way. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
The initial HyperEVM token bootstrap as a gap-fill: the same self-scoping shape as the executed Ethereum gap-fill, joined against the (all-zero) productionTokensHyperEvm() table — so the first dispatch deploys the whole 29-token canonical set, a partial failure recovers by re-dispatch, a token added pre-execution is picked up automatically, and a fully-hydrated table refuses to mint duplicates (NoMissingTokens). Per token, identical to the executed Ethereum flow: 0.1.1 unified deployer (initialAdmin = deploy key) -> receipt readback -> setAuthorizer(HyperEVM V4 authoriser) -> transferOwnership(HyperEVM Safe). Pre-flight hard-gates encode the RAI-1511 ordering: 0.1.1 core deployed, in-use beacons HyperEVM-Safe-owned (beacon-owner migration), authoriser pin hydrated, Safe pin hydrated — an early dispatch is a typed revert, never a partial deploy. Registered in manual-broadcast (dispatch on `network = hyperevm`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPs1hCTxusmaSeFKvoc4Kr
|
Warning Review limit reached
Next review available in: 4 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
f7bcbba to
724af78
Compare
`20260722-deploy-missing-tokens-ethereum` and `-hyperevm` were byte-identical apart from the chain each hardcoded. Both are replaced by `20260807-deploy-missing-tokens`, which resolves the target chain's token table, authoriser and Safe from `block.chainid`. Onboarding a target chain is now a table plus an authoriser pin rather than another copy of the script. "Missing" is also redefined. It was: a canonical config row whose entry in the target chain's table is all-zero, joined by index, which required every table to be the same length at all times and so required a placeholder row to exist before a token could be deployed anywhere. It is now: an `underlying` present in `productionTokensBase()` and absent from the target chain's table, matched by key. Base is the source of truth for what the token set is - a ticker is deployed there first, pinned, then copied outward - so the target chain's table simply lags Base until this script runs. Matching by key rather than index is what lets the two tables carry different lengths in that window, which in turn means a new ticker needs no placeholder anywhere: it is added to the config and Base tables when it is deployed on Base, and the copy picks it up from there. Cross-chain parity is red between the Base pin and the copy landing. That is accurate rather than unfortunate - the chains genuinely differ in that window - so nothing is taught to tolerate it. Dispatching against Base, or any chain with no token table, reverts `UnsupportedTargetChain` rather than resolving to an empty table and reading as "copy everything". The two superseded entries are removed from the `manual-broadcast` script dropdown. That dropdown is documented as append-only so an auditor can re-run a historical script's pre-flight; the entries are removed here because the scripts they name no longer exist.
724af78 to
aba3bb9
Compare
The Graphite base this branch sat on (`graphite-base/291`) was a rebased copy of the HyperEVM stack, all of which has since landed on main under different SHAs. Merging main in re-introduced the two per-chain scripts this branch deletes, so the merge is resolved to main's tree with this branch's own change re-applied on top: the diff against main is exactly the script merge and nothing else. Two main-only commits touched files this branch deletes, and both are subsumed rather than lost: - `5248ac4` corrected six HyperEVM doc comments that had carried the Ethereum sibling's chain name. The unified script resolves its chain from `block.chainid`, so it has no per-chain doc text to get wrong. - `95c4331` extracted `DeployMissingTokensHyperEvmHarness` into its own file for the one-contract-per-file rule. The unified test already puts its harness in `test/script/DeployMissingTokensHarness.sol`. `main` has also grown `productionTokensBase()` to 29 rows since this branch forked; the selection is by key, so the new script picks that up with no change.
`DeployMissingTokensEthereumHarness.sol` is deleted by the script merge; the mirror it names is now `DeployMissingTokensHarness.sol`.
…able Five review findings on the unified token-copy script. The config/Base length check was strict equality, so the moment a ticker is added to `productionTokenConfigs()` ahead of its Base deploy — the state the seeding work creates — every dispatch reverted `TokenTableMisaligned(29, "<length>", "<length>")`: "the tables have drifted out of row alignment" for a state where nothing has drifted, the config table is simply ahead. The equality had no safety role, since the selection loop is bounded by `base.length`; only a config table SHORTER than Base is unsafe, because a Base row would then have no name/symbol to deploy under. That direction now reverts its own `TokenTableTooShort(configsLength, baseLength)` instead of borrowing the misalignment error, whose `index` named a row that does not exist and whose two underlying params were filled with `"<length>"`. Neither alignment guard was reachable from a test: `_selectMissing` read both tables straight from the libraries, so no caller could express a drifted pair. They are the only thing standing between a drifted table and deploying a token under the wrong underlying's name and symbol, which makes them the branches most worth exercising. Both tables are parameters now — `run()` passes the canonical ones — and three tests cover the space: a config table two rows ahead of Base selects only Base's rows, a short one reverts `TokenTableTooShort`, and two swapped config rows revert `TokenTableMisaligned` at row 0. Deleting the per-chain scripts also deleted the only record of manual-broadcast run 30114307165, which deployed all 29 HyperEVM tokens; the Ethereum leg's run id survived only because it sits inline in the RKLB row comment. It is pinned into `productionTokensHyperEvm()`'s NatSpec now, next to the tuples it produced. Finally, `manual-broadcast.yaml` still read "never reorder or delete" while this branch deletes two entries. The rule now carries the exception the deletion was made under — a named successor covering the deleted script's pre-flight at least as strongly, plus the run id carried into the pin — which is what re-dispatching `20260807-deploy-missing-tokens` at the same network does: the same gates, answered against Base's current set rather than the config table the deleted scripts joined against.
All four gaps trace to the same place: the gap-fill scripts this one merges were authored from `20260706-deploy-tokens-ethereum` but dropped three of its checks, and the unified script inherited the omissions. The deploy loop wired each vault onto the authoriser and transferred it to the Safe without reading either back. `20260706` asserts both per token and says why in its own NatSpec: a silent miss finishes the broadcast leaving a live production vault either inoperable or owned by the CI deploy key, and nothing catches it until the pin PR's acceptance tests run. `assertHandoffLanded` is ported with the same two typed errors and, like the original, is public so the assertion can be driven directly — one reachable only from inside a broadcast loop cannot be shown to fire. A deploy call emitting no `Deployment` event left `receiptVault` at zero and carried it into the receipt readback, faulting as a raw call to an empty account mid-broadcast with earlier tokens already live. `20260706` names that case `DeploymentEventMissing`; the event fishing moves into `_readDeployment`, which reverts it. Taking the logs as a parameter is what makes the miss testable at all, and it also pins two behaviours the inline loop only implied: a `Deployment` from another emitter, and another event from the deployer, are both ignored rather than mistaken for this deployment. `_assertAuthoriserReady` had no coverage on any of its four branches despite being one of the guard rails the change advertises. The negative paths are driven through the harness; the positive one rebuilds the EIP-1167 runtime from the 0.1.1 authoriser implementation and etches it, which additionally pins what the codehash MEANS. `LibAuthoriserInvariants` states in prose that `STOX_PROD_AUTHORISER_V4_CLONE_CODEHASH` is the runtime embedding the audited implementation and every consumer then compares hashes to it; nothing checked the derivation, so a clone of a different implementation would have satisfied all of them. It now fails one test. Finally, only the first of the three `_assertDeployer` calls was reached, so a partial 0.1.1 bootstrap — deployer landed, a beacon-set deployer not — would have passed the gate and faulted inside the broadcast. 22 tests, up from 10.
|
Reviewed 0d2aa12: approve Restacked onto main (the Graphite base was a rebased copy of the HyperEVM Both main-only commits that touched the deleted files are subsumed rather than Five review findings addressed in Four coverage gaps closed in One thing this does not close, filed as #297: every live-state check starts from |
|
Reviewed 0d2aa12: approve Rulings-conformance: checked against the repo CLAUDE.md convention sections and Repo CLAUDE.md:
Instructions stated for this work:
The one thing knowingly left open is #297: the pinned tables are the query set |

Merges the two per-chain token-copy scripts into one.
20260722-deploy-missing-tokens-ethereumand-hyperevmwere byte-identical apart from the chain each hardcoded; both are replaced by20260807-deploy-missing-tokens, which resolves the target chain's token table, authoriser and Safe fromblock.chainid.Net −180 lines. Onboarding a target chain is now a table plus an authoriser pin, rather than another copy of the script.
"Missing" is redefined
Before: a canonical config row whose entry in the target chain's table is all-zero, joined by index.
After: an
underlyingpresent inproductionTokensBase()and absent from the target chain's table, matched by key.Base is the source of truth for what the token set is — a ticker is deployed there first, pinned, then copied outward — so a target chain's table simply lags Base until this script runs.
The index-join required every table to be the same length at all times. That meant a ticker could not exist on Base without simultaneously existing as an all-zero placeholder row on every other chain, and it made a partially-filled table an error state that the invariants and the parity pin both had to be taught to tolerate. Matching by key removes all of that: the tables are allowed to differ in length in the window between a Base deploy and its copies, and a new ticker is simply added to the config and Base tables when it is deployed on Base.
Cross-chain parity is red in that window. That is accurate rather than unfortunate — the chains genuinely do differ until the copy lands — so nothing here teaches the suite to tolerate it.
Rollout this supports
productionTokensBase().Dispatch
Actions → manual-broadcast,script = 20260807-deploy-missing-tokens,networkset to the target chain.One dispatch covers one chain, because the workflow supplies a single RPC per run. The selection is chain-generic, so the same script serves every target chain.
Per token the deploy is identical to the scripts it replaces: deploy via the 0.1.1 unified deployer (initialAdmin = deploy key) → read back the ERC-1155 receipt →
setAuthorizer(target chain's V4 authoriser)→transferOwnership(target chain's token-owner Safe), in one deploy-key broadcast with no Safe signature. Each(underlying, receipt, receiptVault, wrapped)tuple is logged for the pin.Guard rails
NoMissingTokens— the target already has every Base token. Also what stops a re-dispatch minting duplicates.UnsupportedTargetChain(chainId)— dispatched against Base (the source, not a target) or a chain with no token table. Rejected rather than resolving to an empty table, which would otherwise read as "copy everything".TokenTableMisaligned(index, ...)— the config table and the Base table have drifted out of row alignment. Every name/symbol deployed is read from the config row at the Base row's index, so a misaligned pair would deploy a token under the wrong underlying's strings.AuthoriserNotReady(address)— the chain's V4 authoriser is unpinned, has no code, or has the wrong codehash.Reviewer note — an append-only convention is broken here
manual-broadcast.yaml's script dropdown documents itself as append-only, so that a signer or auditor can re-dispatch a historical script's pre-flight against on-chain reality. Merging the scripts deletes the two files, so their dropdown entries are removed too.The alternative is to keep both files as executed historicals and let the new script supersede them going forward. Flagging it rather than deciding it silently.
Verification
CI steps run locally in
nix developbefore pushing:forge build— cleanrainix-sol-static— slither 0 findings,forge fmt --checkcleanrainix-sol-legal— REUSE compliant769 tests passing. Seven new tests cover the selection: an empty target (copies everything), a target missing one token (copies exactly that one, chosen by dropping a row from a copy of Base rather than hardcoding a ticker so it does not go stale), a fully-matched target (
NoMissingTokens), per-chain table resolution, and both rejection paths. Failures were diffed against a baseline run of the parent branch; the only non-RPC failures are pre-existing on this stack and unrelated to this change.