Skip to content

Create every fork before selecting any - #158

Open
thedavidmeister wants to merge 1 commit into
mainfrom
2026-08-27-issue-157-create-forks-up-front
Open

Create every fork before selecting any#158
thedavidmeister wants to merge 1 commit into
mainfrom
2026-08-27-issue-157-create-forks-up-front

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Closes #157

The defect

LibRainDeploy and RainDeployVerifyChain walked their network lists with
vm.createSelectFork inside the loop. Foundry captures the account set of the
pre-fork EVM when the first fork is selected, and seeds every fork created
after
that capture with it. With createSelectFork in the loop, networks
2..n are all created on the wrong side of that capture, so they inherit whatever
the default 31337 EVM held for any address the calling script had already
touched — an empty account.

A dep.code.length read in a deploy script, typically added for logging, is
enough. The first network read its chain; every network after it reverted
MissingDependency against a dependency that demonstrably has code there.
Forks created before the capture read their own chain, which is exactly why
the first network was always right.

The fix

Create every fork up front, then select each in turn. LibRainDeploy.createForks
creates and selects nothing; three call sites use it:

  • LibRainDeploy.deployToNetworks
  • LibRainDeploy.checkResolvedAddressesOnNetworks
  • RainDeployVerifyChain.checkDeployedOnSupportedNetworks

This puts every network on the same side of the capture as the first one already
was.

Closed in the library rather than by a rule about what a deploy script may read,
because the trap is invisible from where a consumer sits: the read is ordinary,
the failure names a real address on a network that really has it, and nothing
connects the two.

Alternatives rejected

  • vm.makePersistent on the dependencies pins the empty account across
    every fork. It makes the bug unconditional rather than fixing it.
  • rollFork does not re-seed the account set at all.
  • vm.rpc sidesteps forking but changes the dependency check's semantics.
    The check is meant to read the same fork the deploy then broadcasts on, so
    that a transient RPC inconsistency between two separate reads cannot report a
    live dependency as missing and abort an otherwise-valid deploy.

Two limits, stated plainly

1. It cannot rescue a caller that has already selected a fork before calling.
The capture happens at the first select. A caller that selects a fork first has
already triggered it, so createForks runs afterwards and its forks inherit the
poisoned set. There is no in-library fix for that ordering.

This does not affect the reported path. RainDeployBroadcast.run() forks nothing
before calling deployAndBroadcast: checkCandidatesAnchoredToSource() and
suiteByName() are both internal pure, and vm.envUint does not fork.

2. Endpoint reachability is now all-network. Every alias must be forkable
before any network is touched, so one unreachable endpoint fails the run at fork
creation rather than after the earlier networks have already been checked and
their deploys simulated. It is all-or-nothing on reachability instead of
stopping partway down the list.

To be precise about what that does not change: it does not change what
reaches a chain. Verified locally against two anvils, with a reachable first
endpoint and an unreachable second one:

shape fails at first chain's block after the run
createSelectFork in the loop (before) the second iteration, after the first network's deploy was simulated and recorded 0x0
createFork up front (after) fork creation, before any network's work 0x0

forge script sends only after the whole script has executed, so an aborted
script broadcast nothing under either shape. What changes is how far the run
gets, and therefore what an operator sees before the endpoint error — not what
lands on chain.

A consequence worth naming: paths that used to short-circuit now contact every
endpoint. The chain matrix that reverts on the first network previously never
forked the remaining six; it now forks all seven before checking any. That is
inherent to the fix, and it raises this suite's exposure to a flaky endpoint
(see QA).

QA

  • Discriminating tests: testDeployToNetworksIgnoresAPreForkDependencyRead, testCheckResolvedAddressesOnNetworksCreatesEveryForkFirst, testChainMatrixCreatesEveryForkFirst, testCreateForksSelectsNothing, testCreateForksIgnoresAPreForkRead - each fails on base, verified by reverting its own call site to createSelectFork and running the whole suite (table below), not by reasoning about it.
  • Mutations applied: LibRainDeploy.deployToNetworks -> createForks+selectFork reverted to createSelectFork in the loop -> killed by testDeployToNetworksIgnoresAPreForkDependencyRead; LibRainDeploy.checkResolvedAddressesOnNetworks -> same revert -> killed by testCheckResolvedAddressesOnNetworksCreatesEveryForkFirst; RainDeployVerifyChain.checkDeployedOnSupportedNetworks -> same revert -> killed by testChainMatrixCreatesEveryForkFirst. Each site reverted individually with the other two left fixed; restores verified byte-exact against pristine copies.
  • Oracle: live chain state, read independently of this library. The Zoltu factory 0x7A0D94F55792C434d74a40883C6ed8545E406D12 does have code on arbitrum, so the mutant's MissingDependency("arbitrum", 0x7A0D...) is a demonstrable false negative rather than an expected result. Fork block numbers come from the endpoints themselves.
  • Category check: Consumers that read dependency state before forking silently break every network after the first #157 asks for one thing - the library must stop networks after the first inheriting a pre-fork read, rather than leaving each consumer to avoid the read. Covered at all three call sites. The issue's second limb, a repo-wide sweep for the same pattern, found no other defect: RainDeployVerifyBase.deriveDeployment does read factoryAddress.codehash pre-fork, but its own snapshotState/revertToState clears the capture, verified rather than assumed; every other .code/.codehash read is behind vm.makePersistent or already inside a fork.

Full suite. forge test — 350 passed, 0 failed, 0 skipped.

Mutation testing. Each of the three changed call sites was individually
reverted to createSelectFork in the loop, with the other two left fixed, and
the whole suite run. Every site has a test that dies without it:

reverted site test that dies failure
RainDeployVerifyChain.sol checkDeployedOnSupportedNetworks testChainMatrixCreatesEveryForkFirst vm.selectFork: No matching fork found for 6
LibRainDeploy.sol checkResolvedAddressesOnNetworks testCheckResolvedAddressesOnNetworksCreatesEveryForkFirst vm.selectFork: No matching fork found for 1
LibRainDeploy.sol deployToNetworks testDeployToNetworksIgnoresAPreForkDependencyRead MissingDependency("arbitrum", 0x7A0D94F55792C434d74a40883C6ed8545E406D12)

The last is the exact failure #157 reports.

Pre-existing HyperEVM flakiness, not from this diff. Across repeated full-suite
runs, rpc.hyperliquid.xyz intermittently answers
error code -32603: invalid block height: <head>, failing whichever test forks
it first that run. It has been seen on testZoltuFactoryCodehash,
testCreditOnAHyperEvmFork, testCreditOnHyperEvmForksTheAliasBeforeItsGuards,
testTheLiveSystemContractIsWhatIsPinned and
testChainMatrixReachesTheLastSupportedNetwork — untouched tests, forking
HyperEVM through createSelectFork as they always did. It is the endpoint
serving a node behind the head, and it is precisely the failure class CLAUDE.md
calls out. Re-running clears it. Noting it because CI may need a re-run, and
because the change does raise the number of HyperEVM forks per run.

Docs

CLAUDE.md's "a fork failure is not a missing deployment" note named
vm.createSelectFork only. The paths it is about now fail under vm.createFork,
so the note names both and records that the failure is now up front.

Summary by CodeRabbit

  • Bug Fixes

    • Network deployment and verification now create all required network forks before running checks or broadcasts.
    • A failure to create any fork is reported before network validation or deployment begins.
    • Pre-fork blockchain reads no longer affect code or dependency checks on subsequently created forks.
  • Tests

    • Added coverage confirming fork creation order and isolation across deployment, verification, and address-resolution workflows.

Foundry captures the pre-fork EVM's account set when the FIRST fork is
selected, and seeds every fork created after that capture with it, so
`createSelectFork` inside a network loop put networks 2..n on the wrong
side of the capture. A `dep.code.length` read in a deploy script, added
for logging, was enough to make every network after the first revert
`MissingDependency` against a dependency that has code there.

Closes #157

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9015dc48-042c-4c60-9e4f-37a5825965d6

📥 Commits

Reviewing files that changed from the base of the PR and between 43a24a1 and 7ea7a3b.

📒 Files selected for processing (5)
  • CLAUDE.md
  • src/abstract/RainDeployVerifyChain.sol
  • src/lib/LibRainDeploy.sol
  • test/src/abstract/RainDeployVerifyChain.t.sol
  • test/src/lib/LibRainDeploy.t.sol

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The change creates all network forks before selecting any fork. Deployment and verification loops then select the pre-created forks. Tests cover fork isolation, pre-fork reads, and failures during the first network check.

Changes

Upfront fork creation

Layer / File(s) Summary
Fork creation helper and isolation tests
src/lib/LibRainDeploy.sol, test/src/lib/LibRainDeploy.t.sol
Adds createForks, which creates paired fork IDs without selecting a fork. Tests cover fork state and pre-fork reads.
Network operation integration
src/abstract/RainDeployVerifyChain.sol, src/lib/LibRainDeploy.sol, test/src/abstract/..., test/src/lib/..., CLAUDE.md
Deployment and network checks create all forks before iteration, then select each fork by ID. Tests verify later forks exist and dependency reads remain valid. Documentation describes the all-network fork preflight.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 7ea7a

The PR changes fork setup so all networks are created before selection and validates the behavior across all affected call sites; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant deployToNetworks
  participant LibRainDeploy
  participant Vm
  deployToNetworks->>LibRainDeploy: createForks(vm, networks)
  LibRainDeploy->>Vm: createFork(network)
  Vm-->>LibRainDeploy: forkId
  LibRainDeploy-->>deployToNetworks: forkIds
  deployToNetworks->>Vm: selectFork(forkIds[i])
  deployToNetworks->>deployToNetworks: validate dependencies and deploy
Loading

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: creating all network forks before selecting any fork.
Linked Issues check ✅ Passed The changes address #157 by creating all forks before dependency, deployment, and network verification checks. The new helper is applied to all listed call sites, and tests verify that pre-fork reads …
Out of Scope Changes check ✅ Passed The changes remain within scope. The implementation, documentation update, and tests directly support the fork initialization fix described in #157.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Full details: Linked Issues check

Explanation

The changes address #157 by creating all forks before dependency, deployment, and network verification checks. The new helper is applied to all listed call sites, and tests verify that pre-fork reads do not poison later forks.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (5 skipped: 5 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-08-27-issue-157-create-forks-up-front

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.

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.

Consumers that read dependency state before forking silently break every network after the first

1 participant