Assert [etherscan] entries resolve and bind each chain id to its network - #229
Conversation
The config group asserted that a [etherscan] key exists per supported network and nothing about the entry. An entry carrying only a key, under an alias foundry does not itself resolve to a chain, satisfied every assertion and is config that verifies nothing: foundry resolves the section rather than the one entry the network needs, so it raises "At least one of `url` or `chain` must be present for Etherscan config with unknown alias" for whichever network --verify was pointed at. Every entry now states `chain`, and the check requires at least one of `chain` or `url` on each. Strict on every alias rather than only the ones foundry cannot resolve, so it needs no knowledge of foundry's alias table. The group's assertions move to RainDeployVerifySnapshotBase as checkNetworksConfigured, which takes the config text, so a test can hand it one; testSupportedNetworksAreFullyConfigured keeps the file read that binds them. Without that, deleting the new assertion's call site was a mutation nothing killed. Closes #192 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
|
Warning Review limit reachedNext included review available in 29 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (7)
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 |
`[etherscan]` now states a `chain` on all nine entries and the config group asserts each entry carries `chain` or `url`, but nothing re-checks that a stated id is the id its network reports. The nine were read off `cast chain-id` once, by hand, and from then on a wrong one resolves, reads as well-formed, passes every check that reads the text — and is what `--verify` submits, so the deployment is verified against another chain's explorer after the gas is spent. `testSupportedNetworkChainIdsAreBound` forks every supported network that states a `chain` and compares it against `block.chainid`. The same comparison catches the mirror case, an `[rpc_endpoints]` alias bound to a different network than it names, which is worse: every chain-anchored assertion ever made through that alias was made somewhere nobody named. In `RainDeployVerifyChain` because the subject is the endpoint. This is the contract that forks, and keeping the comparison out of the snapshot half is what leaves that half bindable by a job with no RPC credentials. It reads the binder's own `foundry.toml`, so the chain half now needs the same `fs_permissions` entry the snapshot half already does. An entry resolving through a `url` alone states no id and is skipped, since the config group requires only one of the two. Every entry being that way is `NoDeclaredChainIds` rather than a check that passes having forked nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
The `@notice` opens by stating what the whole group asserts, and the previous commit added a second assertion family to this contract without extending it. Left alone the sentence names only the deployment half while still reading as the complete list, so the one thing a scope statement exists to answer — what is in this group — is answered wrongly. The README table in that same commit already carries both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
Three conflicts, all where both sides appended to the same list, and all resolved by keeping both: the `FrozenSnapshotAmbiguous` error main adds and the `EtherscanEntryUnresolvable` error this branch adds, and the same pair in the test's import list. `FrozenSnapshotAmbiguous` is placed with the other `FrozenSnapshot*` errors it belongs to and `EtherscanEntryUnresolvable` after them, since it is about a different file. The memkv bump needed a resolution git could not see. This branch MOVED that import into the new `RainDeployVerifySnapshotBase.sol`; main bumped it to 0.1.5 where it used to live. Taking both would leave the moved copy on 0.1.4 — which no longer resolves — and a dead 0.1.5 import in `RainDeployVerifySnapshot.sol`, which uses none of those symbols. So the import stays where this branch put it, at main's version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
The sweep took the rationale as well as the restatement. What comes back is every sentence that says something the code does not: - `foundry.toml`: why `chain` is stated on EVERY entry rather than only on the aliases foundry cannot resolve itself, and that each one is compared against the id its alias reports. The second is this PR's whole subject. - `NetworkChainIdMismatch`, `NoDeclaredChainIds`: what a revert means for a deriving repo — a deployment verified against another chain's explorer, or a check that passes having forked nothing. - `DeclaredChainId`: the member docs, on a struct deriving repos read. - `declaredChainIds`, `checkNetworkChainIds`, `checkEtherscanEntriesResolvable`, `checkNetworksConfigured`: why an entry with no `chain` is not a gap, why the config text is a parameter rather than a read, the fork ordering, why the strict form is monotonic under a toolchain bump, and why membership is asserted in both directions. - `testSupportedNetworkChainIdsAreBound`, `testSupportedNetworksAreFullyConfigured`: why the comparison lives in the chain half, and why `fs_permissions` needs `foundry.toml` in the binder. - the test docstrings that say what each case discriminates — the mismatch that is on the LAST entry, the id no network has, the empty declaration set, the entry that satisfies every membership assertion and still verifies nothing. Left cut, because they restate a signature or a literal: the `@param`/`@return` lines on the `external*` revert wrappers and on `fixtureNetworks`, and the `exampleEtherscanConfig` block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
|
Six conflicts, five of them the same decision: #233 GENERATES the network config from `LibRainDeploy.supportedNetworkConfigs()`, so a check that reads `foundry.toml` back is the generator reading its own output. #229 landed on main in the other direction — it strengthened those reads. Generation wins everywhere the two meet. - `README.md`, `foundry.toml`: the branch's text and the generated blocks. The `[etherscan]` block is emitted, so main's prose inside it and its hand-maintained comment cannot survive there; the rationale for stating `chain` on every entry lives in `LibRainDeployConfig` instead. - `RainDeployVerifySnapshot.sol`: `testSupportedNetworksAreFullyConfigured` is gone. With both sides written from one list there is nothing to compare. - `RainDeployVerifyChain.sol`, `RainDeployVerifyChain.t.sol`: the branch's roster-based `checkNetworkChainIds` is kept and main's `declaredChainIds`, `DeclaredChainId` and `NoDeclaredChainIds` are deleted, for the same reason. Main's prose about what a wrong `chain` costs is kept; so is its property that every entry is checked and not only the first, ported to the roster as `testChainIdChecksEveryEntry`. - `BuildScript.t.sol`: both constant sets, which do not overlap. Two things followed from those resolutions rather than being conflicts. `checkNetworksConfigured` and `checkEtherscanEntriesResolvable` are deleted from `RainDeployVerifySnapshotBase`, with `EtherscanEntryUnresolvable` and their tests. #229 moved the comparison's body there from the test; deleting the test leaves it dead, and it IS the comparison — keeping it would leave a tested, consumer-callable assertion about a file this package now writes. `testRunCallsEveryHookThatRegenerates` becomes `testRunCallsEveryGenerator`. It required `run()`'s calls to be exactly the `internal virtual` hooks that write, and `run()` now also calls `regenerateConfig`, which is deliberately NOT a hook: the roster is this package's own, and a repo able to override the emission would deploy to and verify fewer chains with nothing red. The set it enumerates is now every `internal` function that can write, which holds the same two claims over a strictly larger set and no longer turns on `virtual`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
Closes #192.
What was wrong
testSupportedNetworksAreFullyConfiguredasserted that a[etherscan]keyEXISTS for every supported network and nothing about what the entry contains.
An entry carrying only a
key, under an alias foundry does not itself resolveto a chain, satisfied every assertion in the group and is config that cannot
verify anything — foundry resolves the SECTION rather than the one entry the
network being verified needs, so it raises
for whichever network
--verifywas pointed at.foundry.toml, the testNatSpec and the README each named that failure mode and each placed it outside
what was checked.
The hazard is live, not hypothetical. With
chain = 999dropped from thehyperevmentry onmain,forge verify-contract --chain 137(polygon, adifferent network entirely) dies on that error, while with the entry intact the
same command resolves and reaches the explorer. Both runs on this toolchain
(foundry
43923a4).Stating
chainon all nine entries then creates a second, quieter gap. Thoseids were read off
cast chain-idonce, by hand, and nothing re-reads them. Awrong id resolves, reads as well-formed and passes every assertion in the
group, because every assertion in the group is about the TEXT — and
chainiswhat
--verifysubmits, so the deployment is verified against another chain'sexplorer, after the gas is spent. The same gap swallows the mirror case: an
[rpc_endpoints]alias bound to a different network than it names, which isworse, because every chain-anchored assertion ever made through that alias was
made somewhere nobody named.
What changed
[etherscan]entries now statechainon all nine networks, not only on thefour whose alias foundry cannot resolve. Chain ids were read off the repo's
own
[rpc_endpoints]withcast chain-id, and the four already stated (1,56, 999, 4663) came back matching, which is what validates the other five.
[etherscan]entry carries at leastone of
chainorurl— foundry's own condition, spelled strictly so that itneeds to know nothing about foundry's alias table. Stating the chain an alias
already resolves to resolves it to the same chain, so the strict form is
monotonic and cannot red-line when foundry adds or renames an alias.
RainDeployVerifySnapshotBaseascheckNetworksConfigured(config, networks), leavingtestSupportedNetworksAreFullyConfiguredas the file read that binds them.This is the split the base already documents for
checkFrozenSnapshotsReleased: a check that takes its subject as an argumentcan be handed one a test builds. Without it, deleting the new assertion's call
site was a mutation nothing killed — the check would have existed while
nothing proved it was wired.
testSupportedNetworkChainIdsAreBoundinRainDeployVerifyChainforksevery supported network that states a
chainand compares the declared idagainst
block.chainid, failing withNetworkChainIdMismatch(network, declared, reported). Both ids are in the error because which of the two iswrong — the declaration, or the alias the endpoint is bound to — is not
something the check can know, and they are opposite fixes.
the endpoint, not the text. This is the contract that already forks, and
keeping the comparison out of the snapshot half is what leaves that half
bindable by a job with no RPC credentials.
urlalone states no id and is skipped, sincethe config group requires only one of the two of it. Every entry being that
way is
NoDeclaredChainIdsrather than a check that passes having forkednothing.
foundry.toml, the test NatSpec and the README passages updated: entry shapeand the id binding are both inside the declared scope now, so the places that
said the keys were the whole contract no longer say it, and the group table
says what the chain group catches.
Two consumer-visible notes. A consumer repo whose
[etherscan]section has anentry with neither
chainnorurlgoes red on bumping, which is the point;the fix is to add
chain = <id>to that entry. AndRainDeployVerifyChainnowreads the binder's own
foundry.toml, so the{ access = "read", path = "./foundry.toml" }entry infs_permissionsthat the snapshot half alreadyneeded is needed by the chain half too — a repo binding
RainDeployVerifyhasit already.
Fork budget
Measured rather than assumed, because the new test is inherited rather than
declared once.
testSupportedNetworkChainIdsAreBoundcreates nine forks perconcrete contract that inherits
RainDeployVerifyChain, and four do(
RainDeployVerifyChainTest,RainDeployVerifyChainCandidateTest,RainDeployVerifyChainEmptyTest,RegistryDeployVerifyTest): 36 forkcreations, plus 3 in the negative cases, for 39 more than base.
That is not visible in the wall clock: whole-suite runs against the same
endpoints came in at 66s, 66s and 72s on base and 64s, 65s and 112s here, which
is endpoint variance rather than a trend. It does not move the flake floor
either, and the floor is not zero.
rpc.hyperliquid.xyzintermittently answersinvalid block heightfor a block it has just reported as latest, whichvm.createForksurfaces as a fork that cannot be instantiated — twice in threebase runs (
testChainNotDeployedRevertsForALaterSuite,testChainMatrixCoversEverySupportedNetwork) and once in four here(
testSuitesLiveOnEverySupportedNetwork), all three of them tests this PR doesnot touch. One run here came back with seven fork failures while an immediate
re-run of the identical tree was 528/528. That is the public endpoints at this
fork volume, before and after; CI holds its own, and
rainix / testis greenon this commit.
QA
testSupportedNetworkChainIdsAreBoundis the bindingitself;
testChainIdMismatchReverts,testChainIdMatchPasses,testChainIdChecksEveryDeclaration,testChainIdIsReadFromTheForkedEndpoint,testChainIdNoDeclarationsReverts,testDeclaredChainIdsReadsTheConfigText,testDeclaredChainIdsSkipsEntriesWithNoChain,testDeclaredChainIdsSkipsNetworksWithNoEntryandtestDeclaredChainIdsOfUrlOnlyEntriesIsRefuseddiscriminate the pieces. Forthe earlier half,
testConfigWithResolvableEtherscanEntriesPasses,testConfigWithUnresolvableLastEtherscanEntryRevertsandtestConfigWithUnresolvableMiddleEtherscanEntryReverts. Verified on base bybreaking what they assert rather than by running tests that do not exist
there: with
src/andtest/checked out ata741d90andflarerestatedas
chain = 15, the whole suite is 515 passed, 0 failed — a config thatwould submit Flare deployments to chain 15's explorer is invisible to every
check on base. The same mutation on this commit is
M08below and fails withNetworkChainIdMismatch("flare", 15, 14). For the entry-shape half theequivalent is dropping
chain = 999fromhyperevm, which onmainleaves[PASS] testSupportedNetworksAreFullyConfigured() (gas: 74114)against anunmutated
74198— the gas delta is the shorter config string and confirmsthe entry contents were never read.
mutation-probe0.1.0 (rainlanguage-skills), 9/9 KILLED,0 survived, 0 no-run, 0 harness errors, over two scopes, plus the earlier
pass on
RainDeployVerifySnapshotBase(9/9 KILLED) which still holds.src/abstract/RainDeployVerifyChain.sol, scope--match-contract RainDeployVerifyChainTest(baseline green, 21 passed):M01if (declared != reported)->if (false)->testChainIdMismatchRevertsM02select/compare loopi < declared.length->i < 1->testChainIdChecksEveryDeclarationM03revert NoDeclaredChainIds()->return->testChainIdNoDeclarationsRevertsM04vm.selectFork(forkIds[i])deleted ->testChainIdIsReadFromTheForkedEndpointM05count loop'svm.keyExistsToml(...)->true->testDeclaredChainIdsSkipsEntriesWithNoChainM06network: networks[i]->network: ""->testDeclaredChainIdsReadsTheConfigTextM07next++deleted ->testDeclaredChainIdsReadsTheConfigTextfoundry.toml, scope--match-contract RainDeployVerifyChainEmptyTest --match-test testSupportedNetworkChainIdsAreBound(baseline green, 1passed):
M08flarechain = 14->chain = 15->testSupportedNetworkChainIdsAreBoundM09flare = "${FLARE_RPC_URL}"->"${ARBITRUM_RPC_URL}"(the aliasbound to another network) ->
testSupportedNetworkChainIdsAreBound, withNetworkChainIdMismatch("flare", 14, 42161)text of foundry's own error, reproduced live on this toolchain by breaking the
hyperevmentry and watching--verifyfail on polygon. For the ids,cast chain-idagainst each[rpc_endpoints]URL, which is neither the config northe check: arbitrum 42161, base 8453, base_sepolia 84532, bsc 56, ethereum 1,
flare 14, hyperevm 999, polygon 137, robinhood 4663. All nine agree with what
[etherscan]states, which is why the new test is green rather than red — andagreeing is what it is FOR, since nothing now lets them drift apart unnoticed.
[etherscan]entry whichcannot verify fail on the PR rather than after a broadcast — and names the
closing assertion as "every
[etherscan]entry carries at least one ofchainorurl". Covered exactly that, at exactly that strength. The chainid binding is added on top of it by direct ruling, because stating
chainonnine entries is what made the ids worth re-checking and this is the PR that
states them; it covers the declared id, the url-only entry that declares none,
the config where nothing declares one, and the mirror case of an alias bound
elsewhere. Nothing else in either group's contract changed — membership is
still asserted in both directions, by membership, on the same raw text.
Suite: 515 passed / 0 failed at
a741d90, 528 passed / 0 failed here (13 more:nine new cases, and the inherited binding in each of the four contracts).
forge fmt --checkclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN