Skip to content

refactor(script): merge the per-chain token-copy scripts into one - #291

Merged
thedavidmeister merged 18 commits into
mainfrom
feat/new-tokens-hood-orcl-smci-baba-tqqq
Aug 10, 2026
Merged

refactor(script): merge the per-chain token-copy scripts into one#291
thedavidmeister merged 18 commits into
mainfrom
feat/new-tokens-hood-orcl-smci-baba-tqqq

Conversation

@hardyjosh

@hardyjosh hardyjosh commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Merges the two per-chain token-copy scripts into one. 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.

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 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 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

  1. Tokens are deployed on Base and pinned into productionTokensBase().
  2. This script is dispatched per target chain, deploying the difference.
  3. Each run's logged tuples are pinned into that chain's table, and parity goes green.

Dispatch

Actions → manual-broadcast, script = 20260807-deploy-missing-tokens, network set 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 develop before pushing:

  • forge build — clean
  • rainix-sol-static — slither 0 findings, forge fmt --check clean
  • rainix-sol-legal — REUSE compliant

769 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.

Josh Hardy and others added 13 commits July 24, 2026 14:29
…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
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thedavidmeister, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee7f8f0c-fa8a-4b03-a852-19544ccc1a74

📥 Commits

Reviewing files that changed from the base of the PR and between 7dee561 and 0d2aa12.

📒 Files selected for processing (13)
  • .github/workflows/manual-broadcast.yaml
  • CHANGELOG.md
  • script/20260722-deploy-missing-tokens-ethereum.s.sol
  • script/20260722-deploy-missing-tokens-hyperevm.s.sol
  • script/20260807-deploy-missing-tokens.s.sol
  • src/lib/LibTokenInvariants.sol
  • test/script/20260722-deploy-missing-tokens-ethereum.t.sol
  • test/script/20260722-deploy-missing-tokens-hyperevm.t.sol
  • test/script/20260807-deploy-missing-tokens.t.sol
  • test/script/DeployMissingTokensEthereumHarness.sol
  • test/script/DeployMissingTokensHarness.sol
  • test/script/DeployMissingTokensHyperEvmHarness.sol
  • test/script/ProvisionAdditionalServiceSignerHarness.sol

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hardyjosh
hardyjosh force-pushed the feat/new-tokens-hood-orcl-smci-baba-tqqq branch 2 times, most recently from f7bcbba to 724af78 Compare August 7, 2026 13:22
@hardyjosh hardyjosh changed the title feat(tokens): declare HOOD, ORCL, SMCI, BABA, TQQQ with per-chain placeholders refactor(script): merge the per-chain token-copy scripts into one Aug 7, 2026
@hardyjosh
hardyjosh changed the base branch from ops/hyperevm-token-deploy to graphite-base/291 August 10, 2026 09:03
`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.
@hardyjosh
hardyjosh force-pushed the feat/new-tokens-hood-orcl-smci-baba-tqqq branch from 724af78 to aba3bb9 Compare August 10, 2026 10:03
@hardyjosh
hardyjosh changed the base branch from graphite-base/291 to ops/hyperevm-token-deploy August 10, 2026 10:03
@hardyjosh
hardyjosh changed the base branch from ops/hyperevm-token-deploy to graphite-base/291 August 10, 2026 10:11
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`.
@thedavidmeister
thedavidmeister changed the base branch from graphite-base/291 to main August 10, 2026 14:19
…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.
@thedavidmeister

Copy link
Copy Markdown
Contributor

Reviewed 0d2aa12: approve

Restacked onto main (the Graphite base was a rebased copy of the HyperEVM
stack, all since landed under different SHAs), so the diff against main is
exactly the script merge. Verified with git cherry that 12 of the 13 base
commits are patch-identical upstream and that the 13th was superseded by
90c0933 with main strictly ahead on every path it touches.

Both main-only commits that touched the deleted files are subsumed rather than
lost: 5248ac4's six chain-name doc corrections have no analogue in a script
that resolves its chain from block.chainid, and 95c4331's harness
extraction is already the shape the unified test ships.

Five review findings addressed in f639081: the config/Base length check no
longer misfires when the config table legitimately runs ahead of Base,
TokenTableTooShort replaces the misalignment error's "<length>" sentinels,
both alignment guards became reachable from tests, run 30114307165 is pinned
into productionTokensHyperEvm(), and the append-only rule now states the
exception the two deletions were made under.

Four coverage gaps closed in 0d2aa12, all traceable to the gap-fill scripts
having dropped checks that 20260706-deploy-tokens-ethereum carries:
assertHandoffLanded is back, DeploymentEventMissing is back,
_assertAuthoriserReady has coverage on all four branches, and the second and
third _assertDeployer calls are shown to fire. 22 tests, up from 10.

One thing this does not close, filed as #297: every live-state check starts from
the pinned tables, so a vault deployed but never transcribed into them is
invisible — and under the new "missing" definition a re-dispatch would deploy a
duplicate for that ticker. That predates this PR and needs enumeration from
chain to answer.

@thedavidmeister

Copy link
Copy Markdown
Contributor

Reviewed 0d2aa12: approve

Rulings-conformance: checked against the repo CLAUDE.md convention sections and
every instruction stated for this work.

Repo CLAUDE.md:

  • Compiler Settings (=0.8.25 in contracts, ^0.8.25 in libraries): the
    unified script, its harness and its test all carry pragma solidity =0.8.25;
    no library pragma changed. foundry.toml is untouched, so the optimizer /
    via_ir / bytecode_hash settings and the single-profile codehash-pinning
    rule are unaffected — this change adds no deployed contract, so the
    StoxReceiptVault EIP-170 margin is not in play.
  • Deployment (production deploys run the audited version-specific script,
    never current source; candidate is never a deploy target): untouched. The
    script here is an operational manual-broadcast script, not the audited
    DeployProdV4_0_1_1 path, and no generated snapshot changed.
  • Naming Conventions (test helpers must be descriptively named, no
    _foo/_bar/_helper placeholders): the one helper added is
    _deploymentLog, named for what it builds; the script's internals keep the
    leading-underscore visibility convention production code is allowed.
  • License (LicenseRef-DCL-1.0, REUSE-compliant): every added and modified
    file carries the SPDX pair; rainix-sol-legal is green.

Instructions stated for this work:

The one thing knowingly left open is #297: the pinned tables are the query set
for every live-state check, so a deployed-but-never-pinned vault is invisible.
Filed rather than folded in, as instructed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants