Refuse a post-deploy check that has no reads - #92
Conversation
`checkResolvedAddresses` guarded only that `readCalls` and `expectedAddresses` pair up, then looped over them. With both empty the pairing guard passes and the loop body never runs, so the function returns having asserted nothing, and `checkResolvedAddressesOnNetworks` forks every network and reports success across all of them having read nothing at all. `NoResolvedAddressReads` refuses it, which is the same fail-closed rule `NoNetworks` already applies one argument along and `NoDeployCandidates` applies to the candidate list. A consumer that builds its read list from a source that came back empty otherwise gets a green "verified on every network" immediately before migrating onto that deployment. The guard runs AFTER the pairing check, not before it: an unpaired call is a mispairing whichever side is empty, so `(0, n)` stays a `ResolvedAddressesLengthMismatch` that names both lengths, and only the empty PAIR — the one case pairing cannot see — raises the new error. `checkResolvedAddressesOnNetworks` repeats the guard before its fork loop for the reason it already repeats the pairing check there: an empty read set is reported without an RPC round trip, so an outage cannot mask it. Closes #61 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThe PR adds ChangesResolved-address read validation
Merge Risk: ⚪ Minimal · up to The change prevents post-deploy address checks from succeeding when they perform no reads and adds targeted coverage for the empty-read cases. No actionable merge-blocking risk remains after normal checks and review. Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Reviewed 6287d8c: ready — Closes #61. Read the diff independently against the issue. Both guards land where the PR says: CI green (rainix static/legal/test), CodeRabbit reviewed, 0 unresolved threads. Mutation matrix 6/6 killed, and the first matrix was discarded rather than reported when its named killers turned out to be fork tests failing on flaky public RPCs — a guard inside Residual, not blocking and not a defect in this diff: the guard relocates the empty-read hazard to the caller rather than removing it. A consumer that may legitimately have zero reads — |
Closes #61 (audit finding
cov-08).The hole
LibRainDeploy.checkResolvedAddressesguarded only thatreadCallsandexpectedAddressespair up, then looped over them:With both lengths zero the pairing guard passes and the loop body never runs, so the function returns having asserted nothing.
checkResolvedAddressesOnNetworksthen forks every network, logsChecking resolved addresses on network: …for each, and returns success across all of them having read nothing at all.That is the same hazard
NoNetworksalready exists for one argument along — its own test says "an empty target set can never be mistaken for every read checking out" — and the read list had no equivalent. A consumer that buildsreadCallsfrom a source that came back empty (a config, a loop over resolved names) gets a green "verified on every network" immediately before migrating onto that deployment.Nothing pinned it either way:
testCheckResolvedAddressesLengthMismatchRevertsfuzzes undervm.assume(readCallsLength != expectedLength), so equal-and-zero is excluded by construction, and every other call site passed a one- or two-element list.The fix
New error, fail-closed on the empty read set, matching the
NoNetworks/NoDeployCandidateshouse rule:The guard runs after the pairing check, not before it. An unpaired call is a mispairing whichever side happens to be empty, and reporting the empty pair as a "mismatch of zero against zero" would say nothing. So
(0, n)stays aResolvedAddressesLengthMismatchnaming both lengths, and only the empty PAIR — the one case pairing cannot see — raises the new error. Hoisting it above the pairing check would swallow real mispairings; mutant M4 below is exactly that placement, and it is killed.checkResolvedAddressesOnNetworksrepeats the guard before its fork loop, for the reason it already repeats the pairing check there: an empty read set is reported without an RPC round trip, so a network outage cannot mask it.Tests
Three new tests in
test/src/lib/LibRainDeploy.t.sol:testCheckResolvedAddressesNoReadsReverts— an empty read set is refused rather than passing.testCheckResolvedAddressesEmptyPairIsNotALengthMismatch— the empty pair raises the new error, empty-reads-against-a-non-empty-expected-list is stillResolvedAddressesLengthMismatch(0, 1), and one real read still passes. The middle assertion deterministically pins the guard ORDER rather than leaving it to whether the fuzzer happens to draw a zero.testCheckResolvedAddressesOnNetworksNoReadsRevertsBeforeForking— refused before anything is forked, using an unconfigured RPC alias so reaching the fork loop is itself an error.The pre-existing
testCheckResolvedAddressesLengthMismatchRevertsfuzz test is unchanged and still passes, which is the point of the ordering.QA
testCheckResolvedAddressesNoReadsReverts,testCheckResolvedAddressesEmptyPairIsNotALengthMismatch,testCheckResolvedAddressesOnNetworksNoReadsRevertsBeforeForking— each fails on base. Verified via mutants M1 and M5, which delete the added guards and so reproduce base behaviour exactly (literal base does not compile these tests, sinceNoResolvedAddressReadsdoes not exist there); with the guard deleted, M1 is killed by the first two and M5 by the third.mutation-probe, 6/6 KILLED, 0 survived, 0 no-run, 0 harness errors, against a green 13-test baseline.checkResolvedAddresses: delete the empty-read guard entirely →testCheckResolvedAddressesNoReadsReverts,testCheckResolvedAddressesEmptyPairIsNotALengthMismatchcheckResolvedAddresses:readCalls.length == 0→== 1→testCheckResolvedAddressesNoReadsReverts,…Match,…MismatchReverts,…DirtyWordReverts,…EmptyPairIsNotALengthMismatchcheckResolvedAddresses:NoResolvedAddressReads(target)→(address(0))→testCheckResolvedAddressesNoReadsReverts,testCheckResolvedAddressesEmptyPairIsNotALengthMismatchcheckResolvedAddresses: hoist the guard ABOVE the pairing check →testCheckResolvedAddressesEmptyPairIsNotALengthMismatch(deterministic) andtestCheckResolvedAddressesLengthMismatchReverts(fuzz-dependent — it needs the fuzzer to drawreadCallsLength == 0, which is why the deterministic assertion was added rather than relying on it)checkResolvedAddressesOnNetworks: delete the pre-fork empty-read guard →testCheckResolvedAddressesOnNetworksNoReadsRevertsBeforeForkingcheckResolvedAddressesOnNetworks:NoResolvedAddressReads(target)→(address(0))→testCheckResolvedAddressesOnNetworksNoReadsRevertsBeforeForking--match-test CheckResolvedAddressesminus the two fork tests) and was run with no.envpresent at all, so nothing in it can reach an RPC. A first pass over the fullforge testproduced 6/6 "killed" whose named killers were fork tests (testChainMatrix*,testDeployToNetworks*) that a guard insidecheckResolvedAddressescannot affect — flaky public-RPC failures being scored as kills. That matrix was discarded as untrustworthy rather than reported.NoNetworksalready encodes one argument along andNoDeployCandidatesencodes for the candidate list ("a candidate the loop never reaches is a contract whose snapshot nothing anywhere anchors"). Expected revert data is built from the error signature viaabi.encodeWithSelector, not read back from a run.checkResolvedAddresses, (c) the guard before the fork loop incheckResolvedAddressesOnNetworks, (d) tests covering all three. All four covered. One deliberate deviation from the issue's proposed patch, which the issue's own verification block flagged: the guard is placed AFTER the pairing check, not before it, so the existing length-mismatch fuzz test keeps its meaning for(0, n).Full suite green: 218 passed, 0 failed, 17 suites.
forge fmt --checkclean.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests