Skip to content

fix(deploy): check the derived Zoltu address before skipping a network - #21

Merged
thedavidmeister merged 7 commits into
mainfrom
2026-07-29-issue-16-stale-expected-address
Jul 30, 2026
Merged

fix(deploy): check the derived Zoltu address before skipping a network#21
thedavidmeister merged 7 commits into
mainfrom
2026-07-29-issue-16-stale-expected-address

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Refs #16

deployToNetworks decided whether to deploy from expectedAddress.code.length
alone, never checking that expectedAddress is where creationCode actually
lands. The Zoltu factory is CREATE2 over its calldata with a zero salt
CALLDATACOPY then CREATE2(callvalue, 0, calldatasize, 0) — so that address
is a pure function of the creation code and is computable in-library. New
zoltuAddress derives it, and deployToNetworks asserts it equals
expectedAddress before forking any network, so drifted constants fail loudly
instead of matching the old contract on every network and exiting 0.

Three existing deployToNetworks revert tests passed hex"" creation code
alongside address(0) — an input pair the library can never satisfy. They now
pass a coherent pair; their asserted revert selectors and arguments are
unchanged.

QA

  • Discriminating tests: testDeployToNetworksStaleExpectedAddressReverts, testDeployToNetworksStaleExpectedAddressRevertsBeforeForking, testZoltuAddressMatchesFactoryDeploy, testDeployToNetworksFactoryReportsOtherAddressReverts — each fails on base (verified by neutralising the new guard to if (false), which reproduces base exactly: testDeployToNetworksStaleExpectedAddressReverts was the ONLY failure of 30, "next call did not revert as expected"; the pre-existing testUnexpectedDeployedAddressReverts still passed, confirming it never discriminated this bug. The other three cannot compile on base — zoltuAddress does not exist there.)
  • Mutations applied: bytes1(0xff)0xfe → testZoltuAddressMatchesFactoryDeploy (0xe7EF…≠0xC240…); ZOLTU_FACTORYaddress(0) → same test (0xD196…≠0xC240…); bytes32(0) salt→bytes32(uint256(1)) → same test (0xa0B4…≠0xC240…); keccak256(creationCode)creationCode → same test (0x0D86…≠0xC240…); guard cond→false → testDeployToNetworksStaleExpectedAddressReverts; revert args swapped → that test + testUnexpectedDeployedAddressReverts; guard hoisted above the NoNetworks check → testDeployToNetworksNoNetworksReverts; guard moved inside the fork loop → SURVIVED 29/29, gap closed by the new testDeployToNetworksStaleExpectedAddressRevertsBeforeForking, then killed; post-deploy deployedAddress != expectedAddressfalse → testDeployToNetworksFactoryReportsOtherAddressReverts (so that branch is live, not dead). All restored via git checkout --, tree verified clean between probes.
  • Oracle: the real Zoltu factory on an Arbitrum One fork, not the derivation. MockDeployable0xC24016f209562fc151e5Ab7F88694ED5775feb36 and MockDeployableV20x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B are both what an actual deployZoltu returns on-chain; the expected revert argument is that literal, not a call to zoltuAddress. Third-party cross-check: for empty creation code the derivation yields 0x5DC93B79FBDD6f26Ed9540597C78eD5893F9aC7A, independently equal to the ZOLTU_EMPTY_CREATION_CODE_ADDRESS constant PR test(deploy): close the coverage gaps a whole-repo mutation pass found #19 obtained from a real factory call.
  • Category check: issue asks A) fail loudly when expectedAddress is stale relative to creationCode, on the skip path as well as the deploy path, before any broadcast is simulated; B) confirm the skip branch bypassing dependency and Zoltu-factory checks is still intended. Covered A (checked before any fork, so both branches and no broadcast). Refs rather than Closes because B is a question about intent, not a defect — that behaviour is unchanged here, still documented in the deployToNetworks NatSpec and pinned by testDeployToNetworksSkipsAlreadyDeployedWithMissingDependency, and needs a human design ruling.

Co-Authored-By: Claude noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added deterministic deployment address validation before deployment begins.
    • Deployment now rejects mismatched expected addresses earlier, including when a deployment is skipped because code already exists.
  • Documentation
    • Clarified deployment address verification in the README and API documentation.
  • Bug Fixes
    • Prevented stale or incorrect expected addresses from bypassing validation.
    • Improved deployment verification across multiple networks.

thedavidmeister and others added 2 commits July 29, 2026 13:22
deployToNetworks decided whether to deploy from `expectedAddress.code.length`
alone, so an `expectedAddress` that disagrees with `creationCode` took the skip
branch on every network and reported success without deploying anything.

`zoltuAddress` derives the CREATE2 address the Zoltu factory produces for the
given creation code, and `deployToNetworks` asserts it equals `expectedAddress`
before forking any network.

Co-Authored-By: Claude <noreply@anthropic.com>
A mutation moving the check inside the network loop survived the whole suite.
An unconfigured RPC alias discriminates: the check reports the mismatch without
any network being reachable.

Co-Authored-By: Claude <noreply@anthropic.com>
@thedavidmeister thedavidmeister self-assigned this Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 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: de5bccf7-f1cb-4465-ae4a-cb3272248403

📥 Commits

Reviewing files that changed from the base of the PR and between 2b685a9 and 900a879.

📒 Files selected for processing (4)
  • test/concrete/MockDeployable.sol
  • test/concrete/MockDeployableV2.sol
  • test/concrete/MockReverter.sol
  • test/src/lib/LibRainDeploy.t.sol

Walkthrough

LibRainDeploy now derives and validates Zoltu CREATE2 deployment addresses before network forking or deployment. Tests use derived address helpers, add a distinct deployment mock, and cover deployment, skip, mismatch, and factory-error scenarios.

Changes

Deterministic address validation

Layer / File(s) Summary
Address derivation and pre-fork validation
src/lib/LibRainDeploy.sol, README.md
Adds Zoltu address derivation, validates expectedAddress before forking, and documents the creation-code assertion.
Derived test fixtures and address assertions
test/concrete/*, test/src/lib/LibRainDeploy.t.sol
Pins test compiler versions, adds MockDeployableV2, and derives expected addresses and runtime hashes in tests.
Deployment scenario regression coverage
test/src/lib/LibRainDeploy.t.sol
Updates happy-path, skip, mismatch, stale-address, factory-error, and pre-fork revert tests to use derived expectations.

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

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant deployToNetworks
  participant zoltuAddress
  participant NetworkFork
  participant ZoltuFactory
  deployToNetworks->>zoltuAddress: derive address from creationCode
  zoltuAddress-->>deployToNetworks: return deterministic address
  deployToNetworks->>deployToNetworks: validate expectedAddress
  deployToNetworks->>NetworkFork: fork network
  NetworkFork->>ZoltuFactory: deploy creationCode
  ZoltuFactory-->>NetworkFork: return deployed address
Loading
🚥 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 accurately summarizes the main change: validating the derived Zoltu address before skip logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-07-29-issue-16-stale-expected-address

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.

@thedavidmeister thedavidmeister added the ai:ready AI vetter: passes review, ready for human decision label Jul 29, 2026
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
Reviewed ee1a7c6: ready — Refs #16 (part A only, linkage deliberately Refs since part B is a design question that stays open on the issue) — pre-fork zoltuAddress guard derives the CREATE2 address (zero salt, matching the factory's actual CREATE2(callvalue,0,calldatasize,0)) and fails loudly on stale constants before any skip branch can mask them; derivation pinned against the real factory on a fork, guard ordering mutation-pinned both ways, incoherent hex""/address(0) test inputs made coherent with selectors unchanged.
cost 415 — deploy-path guard + CREATE2 derivation

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

🤖 ai:vetter
vet-protocol 1
Reviewed ee1a7c6: ready — Refs #16 (part A; Refs not Closes is deliberate — part B is a human design question that keeps the issue open) — pre-fork zoltuAddress guard derives the zero-salt CREATE2 address and fails loudly on stale constants before any skip branch can mask them; derivation oracled against the real factory on-fork for two contracts, guard ordering mutation-pinned both ways incl. a disclosed survivor killed by the before-forking test, post-deploy mismatch branch proven live via mockCall.
cost 412 — deploy-path guard + CREATE2 derivation

thedavidmeister and others added 2 commits July 30, 2026 09:40
`LibRainDeploy.t.sol` pinned `MockDeployable`'s Zoltu address 22 times, its
deployed code hash 7 times and `MockDeployableV2`'s Zoltu address twice, all
as literals. Every one of those is the output of a formula the library now
exposes, so each is a copy of a derived value that goes stale the moment the
creation code changes.

`mockDeployableAddress`, `mockDeployableCodeHash` and `mockDeployableV2Address`
are now the single source for each, computed from
`LibRainDeploy.zoltuAddress(type(Mock).creationCode)` and
`keccak256(type(MockDeployable).runtimeCode)`.

`testDeployZoltu` keeps its pinned literal: the live Zoltu factory on an
Arbitrum fork is the oracle there, so taking the expected value from
`zoltuAddress` would check the derivation against itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MockDeployableV2` is a concrete contract, so its pragma is `=0.8.25` rather
than `^0.8.25`, which floats to the newest solc available.

`LibRainDeploy.t.sol` imports every other source file in the build, so the one
exact pragma resolves the whole test compilation to 0.8.25 instead of 0.8.35.
That changes the creation code of the mocks — solc 0.8.25 emits `5f80fd`
(PUSH0 DUP1 REVERT) where 0.8.35 emits `5f5ffd` (PUSH0 PUSH0 REVERT), same
length, different bytes — and so changes the address the Zoltu factory deploys
them to. Every expected address and code hash in the suite is now derived from
the creation code, so all of them follow; `testDeployZoltu`'s pinned literal
is the one value that cannot, and is updated to what the live factory on an
Arbitrum fork returns for the 0.8.25 creation code.

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

Copy link
Copy Markdown
Contributor Author

Two follow-up fixes pushed to this branch

Head is now fb137c6654d6a35e7642701f99934b0215daa7ba. Nothing about the guard, the new tests' assertions, or src/lib/LibRainDeploy.sol changed — the diff is test/src/lib/LibRainDeploy.t.sol and one pragma.

1. The derived constants are now derived — 30 literals removed, 1 kept

The test file pinned, as literals, three values that are all outputs of the CREATE2-over-creation-code formula this PR itself adds:

pinned value occurrences
MockDeployable's Zoltu address 22
MockDeployable's deployed code hash 7
MockDeployableV2's Zoltu address 2

…against 4 calls to zoltuAddress. Three helpers are now the single source for each:

function mockDeployableAddress() internal pure returns (address) {
    return LibRainDeploy.zoltuAddress(type(MockDeployable).creationCode);
}

function mockDeployableCodeHash() internal pure returns (bytes32) {
    return keccak256(type(MockDeployable).runtimeCode);
}

function mockDeployableV2Address() internal pure returns (address) {
    return LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode);
}

type(MockDeployable).runtimeCode is exact for the code hash: bytecode_hash = "none" and cbor_metadata = false in foundry.toml mean there is no metadata trailer, so the artifact's runtime code is byte-for-byte what the constructor leaves on chain. The mocks have no immutables, so runtimeCode is available at all.

The one surviving literal is in testDeployZoltu, whose oracle is the live Zoltu factory on an Arbitrum fork:

address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode);
assertEq(deployed, 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854);

Taking that expected value from zoltuAddress would check the derivation against itself and prove nothing. It is the only hand-written value left in the file.

testZoltuAddressMatchesFactoryDeploy is byte-identical to before — it never held a literal, because its oracle is the factory call. The code hash keeps no literal either, and does not need one: deployToNetworks compares expectedCodeHash against deployedAddress.codehash read from the fork after a real deploy, so testDeployToNetworksMultipleNetworks, testDeployAndBroadcastHappyPath and the skip-path tests already validate keccak256(type(MockDeployable).runtimeCode) against on-chain state.

2. MockDeployableV2 pragma ^0.8.25=0.8.25

It is a concrete contract, so the pragma is exact per the org convention — ^ for library and abstract, = for concrete, test mocks included.

Why the two are coupled, and the one number that moved

^0.8.25 does not mean 0.8.25; it floats to the newest solc the toolchain ships. On the pinned rainix toolchain (RAINIX_SHA=53e96a7d0a97d7c7c75c3b2412521324776fdac6), forge build reports:

  • before: Compiling 24 files with Solc 0.8.35
  • after: Compiling 24 files with Solc 0.8.25

All 24 files, not just the one pinned: LibRainDeploy.t.sol imports every other source file, so forge must pick a single version satisfying every transitive pragma, and =0.8.25 is the binding constraint.

That changes the mocks' creation code. MockDeployable's init code is 148 bytes either way, but 0.8.25 emits 5f80fd (PUSH0 DUP1 REVERT) where 0.8.35 emits 5f5ffd (PUSH0 PUSH0 REVERT), three times over:

0.8.25  0x6080604052602a5f553480156012575f80fd5b50607680601e5f395ff3fe…
0.8.35  0x6080604052602a5f553480156012575f5ffd5b50607680601e5f395ff3fe…

Same length, different bytes, so the CREATE2 address moves:

solc 0.8.35 solc 0.8.25
MockDeployable address 0xC24016f209562fc151e5Ab7F88694ED5775feb36 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854
MockDeployable code hash 0xc1a263a0…620c5483 0x6ea525f6…c78cc299
MockDeployableV2 address 0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B 0xDDB4cB0c3eaCE82Fbb15840CaeBDEc1A0Cdf98c3

This is why the derivation had to land first. With the constants derived, pinning the pragma carried all 30 derived occurrences across automatically and exactly one test failed — the pinned oracle:

[FAIL: assertion failed: 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854
                      != 0xC24016f209562fc151e5Ab7F88694ED5775feb36] testDeployZoltu()

The left-hand value there is what the real Zoltu factory returned on the Arbitrum fork, not the derivation — so the on-chain oracle is what dictated the updated literal, and it stays an independent check rather than a restatement of zoltuAddress.

Doing it in the other order is how #23 went wrong: it pinned the pragmas and then hand-updated 18 derived literals to their new values, which is the same defect re-pinned at a different compiler.

Verification

Repo's own toolchain, matching CI (nix develop github:rainlanguage/rainix/53e96a7d0a97d7c7c75c3b2412521324776fdac6#sol-shell), after forge soldeer install:

  • forge test30 passed, 0 failed, 0 skipped. No fork test was skipped or stubbed: the Arbitrum and Base fork tests all ran against live RPCs, including the full Base archive binary search in testFindDeployBlockZoltuFactory and testIsStartBlockAtDeployBlock.
  • slither . — 4 contracts, 100 detectors, 0 results
  • forge fmt --check — clean
  • rainix-sol-single-contract — clean
  • reuse lint — compliant, 20/20 files

That the derivation reproduces the values it replaced, rather than merely being self-consistent, is pinned by the intermediate commit 1695862: derivation only, still solc 0.8.35, oracle literal still 0xC240…, green 30/30. Same addresses and same code hash as before, now computed instead of copied.

Deliberately left alone

MockDeployable.sol and MockReverter.sol still carry ^0.8.25. They are concrete too, but this PR does not otherwise touch them. Their effective version is already 0.8.25 via the constraint intersection described above, so behaviour is unaffected; making the pragmas say so is a separate sweep.

`zoltuAddress` is the only function this branch adds to
`LibRainDeploy` that declared a named return. The three counterparts
it adds to the test contract — `mockDeployableAddress`,
`mockDeployableCodeHash` and `mockDeployableV2Address` — already
declare unnamed returns with an explicit `return`, and unnamed
returns are the prevailing form across rain Solidity. Declaring
`returns (address)` with an explicit `return`, and the matching
unnamed `@return` tag, makes the four functions the branch adds
consistent with each other and with the rest of the org.

The five pre-existing named returns in this file are deliberately
left alone. Converting one means adding an explicit `return` on
every path of a function whose paths have to be enumerated first,
which is semantic risk for a style win in a deploy library.

No behaviour change: the body is a single unconditional assignment,
so assigning a named return and returning the same expression
produce the same value. Every derived address and code hash, and the
pinned oracle literal in `testDeployZoltu`, are unchanged.

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

Copy link
Copy Markdown
Contributor Author

QA evidence for 528a7c7cf2ad0b6cd65ece23d567fd397f831192

Single-purpose follow-up: zoltuAddress was the only function this branch adds to LibRainDeploy that declared a named return. It now declares returns (address) with an explicit return, and its @return tag takes the matching unnamed form. That is the whole diff — 3 insertions, 3 deletions, one hunk, one file:

-    /// @return derivedAddress The address the creation code deploys to.
-    function zoltuAddress(bytes memory creationCode) internal pure returns (address derivedAddress) {
-        derivedAddress = address(
+    /// @return The address the creation code deploys to.
+    function zoltuAddress(bytes memory creationCode) internal pure returns (address) {
+        return address(

This makes the four functions the branch adds internally consistent — mockDeployableAddress, mockDeployableCodeHash and mockDeployableV2Address were already unnamed-with-explicit-return — and consistent with the prevailing unnamed-return form across rain Solidity.

Scope: the five pre-existing named returns are untouched

isStart (isStartBlock), deployBlock (findDeployBlock), and deployedAddress in deployZoltu, deployToNetworks and deployAndBroadcast are all still named, verified after the edit:

79:        returns (bool isStart)
106:        returns (uint256 deployBlock)
168:    function deployZoltu(bytes memory creationCode) internal returns (address deployedAddress) {
237:    ) internal returns (address deployedAddress) {
329:    ) internal returns (address deployedAddress) {

Converting any of those means adding an explicit return on every path of a function whose paths have to be enumerated first — semantic risk for a style win, in a deploy library. Not done. zoltuAddress is safe to convert precisely because its body is a single unconditional assignment with no branch, no early exit and no post-assignment mutation, so the named return was not load-bearing.

Tests: 30/30 before, 30/30 after, nothing skipped

Before (fb137c6):

Suite result: ok. 30 passed; 0 failed; 0 skipped; finished in 10.91s

After (528a7c7):

Suite result: ok. 30 passed; 0 failed; 0 skipped; finished in 8.08s

Same 30 tests, same names, no test added, removed, renamed or skipped. The fork tests ran against live RPCs — testFindDeployBlockZoltuFactory binary-searches Base history from block 0 and testDeployZoltu / testZoltuAddressMatchesFactoryDeploy hit the live Arbitrum factory. None skipped.

No derived value and no oracle literal moved

Not just "the tests still pass" — the derived values were recomputed independently of the Solidity, with cast create2 over the compiled creation code, before and after the edit:

value before after
MockDeployable derived address 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854
MockDeployable runtime code hash 0x6ea525f6523fe148810254f04b9a74a379f59ca0f3a7fa83db90b691c78cc299 0x6ea525f6523fe148810254f04b9a74a379f59ca0f3a7fa83db90b691c78cc299
MockDeployableV2 derived address 0xDDB4cB0c3eaCE82Fbb15840CaeBDEc1A0Cdf98c3 0xDDB4cB0c3eaCE82Fbb15840CaeBDEc1A0Cdf98c3
MockDeployableV2 runtime code hash 0x62d58ae725f485b042d3fcac2842a3cd945a78e77b45b0f5d110cf6660c60311 0x62d58ae725f485b042d3fcac2842a3cd945a78e77b45b0f5d110cf6660c60311

All four byte-identical. Separately, git diff HEAD~1 -U0 contains no hex literal on any changed line at all — no address, no code hash, no test expectation is inside the diff.

The one surviving pinned literal, the live-fork oracle 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854 in testDeployZoltu, is unchanged. Note it independently equals the cast create2 derivation above, computed outside the library — so the oracle and the derivation agree via a third path, not only via zoltuAddress checking itself.

No pragma was touched; the test build still resolves to solc 0.8.25 via MockDeployableV2's =0.8.25 pin, which is what those addresses are a function of.

Checks run locally (all in the repo's nix sol-shell)

check result
forge test 30 passed, 0 failed, 0 skipped
forge fmt --check clean, exit 0
slither . 4 contracts with 100 detectors, 0 result(s) found
reuse lint REUSE 3.3 compliant, 20/20 files, 0 missing licenses
no-custom-natspec no @custom: tags
no-ignored-tests no vm.skip, no xtest
no-submodules no .gitmodules
one contract per file 1 per file across all 5 .sol files

rainix-sol-single-contract is not on PATH in this repo's flake-pinned sol-shell (CI invokes it from rainix at RAINIX_SHA=53e96a7d), so its gate was verified by hand as the last row rather than by running the binary.

… version

The org pragma convention is `=` for concrete contracts including
concrete test mocks. `MockDeployableV2` already pins `=0.8.25`;
`MockDeployable` and `MockReverter` floated `^0.8.25`.

The float matters to this branch because `testDeployZoltu`'s pinned
oracle literal is the address the live Zoltu factory returns for the
creation code solc 0.8.25 emits for `MockDeployable`. With `^0.8.25`,
`MockDeployable`'s own pragma admits newer compilers under which that
literal is wrong; its effective version was only held at 0.8.25 by
`MockDeployableV2`'s `=0.8.25` pin constraining the shared test build.
Pinning the two siblings makes each mock's compiler pin direct, and the
`testDeployZoltu` comment now cites `MockDeployable`'s own pin instead
of the other file's side effect.

The build already resolved 0.8.25, so nothing moves: creation code,
runtime code hashes and derived addresses are byte-identical before
and after, and the oracle literal is untouched.

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

Copy link
Copy Markdown
Contributor Author

QA evidence for 2b685a9b30f5c2e57f19bb069b18bcb6a9241a8d

Pragma-pin follow-up. The org convention is = for concrete contracts including concrete test mocks; MockDeployable and MockReverter floated ^0.8.25 while MockDeployableV2 pinned =0.8.25. That float is load-bearing for this branch: testDeployZoltu's oracle literal is only valid under solc 0.8.25, and MockDeployable's effective compiler was held there by a different file's pragma (MockDeployableV2's =0.8.25 constraining the shared test build) rather than its own. Under ^0.8.25 alone, a newer compiler is admissible and the literal would be wrong.

The whole diff — exactly three files, 3 insertions, 4 deletions

  1. test/src/lib/MockDeployable.sol:3pragma solidity ^0.8.25;pragma solidity =0.8.25;
  2. test/src/lib/MockReverter.sol:3pragma solidity ^0.8.25;pragma solidity =0.8.25;
  3. test/src/lib/LibRainDeploy.t.sol — the testDeployZoltu comment now cites the pragma that actually holds the oracle in place:
         // It is the address the factory returns for the creation code solc
-        // 0.8.25 emits for `MockDeployable`, which is the version the whole
-        // test build resolves to because `MockDeployableV2` pins `=0.8.25`.
+        // 0.8.25 emits for `MockDeployable`, which itself pins `=0.8.25`.

Nothing else: the five pre-existing named returns are verified still present (:79 isStart, :106 deployBlock, :168/:237/:329 deployedAddress), no other comment touched, src/lib/LibRainDeploy.sol untouched in this commit.

Nothing moved — proved, not assumed

The build already resolved 0.8.25 (and foundry.toml sets cbor_metadata = false, bytecode_hash = "none", so pragma text never enters bytecode). Recomputed independently of the Solidity with cast create2 / cast keccak over the compiled artifacts, before and after — six values this time, adding the creation-code hashes of the two files whose pragmas changed:

value before after
MockDeployable derived address 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854 identical
MockDeployable runtime code hash 0x6ea525f6523fe148810254f04b9a74a379f59ca0f3a7fa83db90b691c78cc299 identical
MockDeployable creation code hash 0x2f579dc9bdd37f6d125494e0ffd9ccd0ee729ed40f356329e95ec8f86047cba8 identical
MockReverter creation code hash 0xf433f3015cb568cc8dca65e047a57bfdd17df704a7fa8eb1499fbecbc8228c07 identical
MockDeployableV2 derived address 0xDDB4cB0c3eaCE82Fbb15840CaeBDEc1A0Cdf98c3 identical
MockDeployableV2 runtime code hash 0x62d58ae725f485b042d3fcac2842a3cd945a78e77b45b0f5d110cf6660c60311 identical

All six byte-identical (diff empty after stripping terminal control bytes from the captures). Separately, git diff HEAD~1 -U0 contains no hex literal on any changed line — the oracle literal 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854 appears in the hunk only as unchanged context.

Tests: 30/30 before, 30/30 after, nothing skipped

Suite result: ok. 30 passed; 0 failed; 0 skipped; finished in 9.70s

Same 30 test names as the previous head. Fork tests ran live (Base archive binary search, live Arbitrum Zoltu factory) — none skipped.

Checks run locally (repo nix sol-shell)

check result
forge test 30 passed, 0 failed, 0 skipped
forge fmt --check clean, exit 0
slither . 4 contracts with 100 detectors, 0 result(s) found
reuse lint REUSE 3.3 compliant, 20/20 files

Resulting pragma state: =0.8.25 on all three concrete mocks; ^0.8.25 on the library. (LibRainDeploy.t.sol is itself a concrete test contract still on ^0.8.25 — observed but deliberately not touched, it is outside this change's scope.)

The org tree convention is that `test/src/**` mirrors `src/**` — `.t.sol`
files only, each at its subject's path — while `test/{lib,concrete,abstract}`
holds the suite's own support code placed by its own kind. The three mocks
are concrete support contracts, not tests of anything under `src/lib/`, so
they belong in `test/concrete/`, not inside the mirror tree in a `lib/`
directory. Not `test/src/concrete/`: this repo has no `src/concrete/`, so
that path would claim tests of a source tree that does not exist.

`LibRainDeploy.t.sol` stays at `test/src/lib/` — its subject is
`src/lib/LibRainDeploy.sol`, so that is its correct mirror home. Its three
mock imports follow the move, and the `contractPath` string literals passed
to `deployToNetworks`/`deployAndBroadcast` name the real new path, since
they feed `forge verify-contract` commands.

Pure moves: `foundry.toml` sets `cbor_metadata = false` and
`bytecode_hash = "none"`, so source paths never enter bytecode. Creation
code, runtime code hashes and derived addresses for all three mocks are
byte-identical before and after, and the oracle literal in
`testDeployZoltu` is untouched.

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

Copy link
Copy Markdown
Contributor Author

QA evidence for 900a879cef164b64279c117b3922c3179efb3093

Tree-convention follow-up: test/src/** is the mirror tree — .t.sol files only, each at its subject's path — while test/{lib,concrete,abstract} holds the suite's own support code placed by its own kind. The three mocks are concrete support contracts and were sitting inside the mirror tree in a lib/ directory; they now live in test/concrete/.

The whole diff — 3 pure renames + 9 line edits in one file

rename test/{src/lib => concrete}/MockDeployable.sol (100%)
rename test/{src/lib => concrete}/MockDeployableV2.sol (100%)
rename test/{src/lib => concrete}/MockReverter.sol (100%)
test/src/lib/LibRainDeploy.t.sol | 18 +++++++++---------

Git itself attests the renames at 100% similarity — not one byte of the moved files changed; SPDX headers and the =0.8.25 pins travelled with them. The nine edited lines in LibRainDeploy.t.sol are exactly: the three mock imports ("./MockX.sol""../../concrete/MockX.sol") and all six occurrences of the two contractPath string literals ("test/src/lib/MockDeployable.sol:MockDeployable" ×5 and the V2 equivalent ×1 → "test/concrete/..."), which must name the real path because they feed the printed forge verify-contract commands. A residual-reference grep over all .sol files finds zero stale test/src/lib/Mock* or "./Mock* strings.

LibRainDeploy.t.sol itself stays at test/src/lib/ — its subject is src/lib/LibRainDeploy.sol, so that is its correct mirror home. Deliberately not test/src/concrete/: this repo has no src/concrete/, so that path would claim tests of a source tree that does not exist.

Nothing moved — proved, not assumed

foundry.toml sets cbor_metadata = false and bytecode_hash = "none", so source paths never enter bytecode — and the artifacts confirm it. Recomputed independently with cast keccak / cast create2 over the compiled artifacts before and after, seven values covering all three moved files:

value before = after
MockDeployable creation code hash 0x2f579dc9bdd37f6d125494e0ffd9ccd0ee729ed40f356329e95ec8f86047cba8
MockDeployableV2 creation code hash 0x70bb1d6f105fa71822c613fdd529385edd76b8f9866eb6a04ff952a39d775c6a
MockReverter creation code hash 0xf433f3015cb568cc8dca65e047a57bfdd17df704a7fa8eb1499fbecbc8228c07
MockDeployable derived address 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854
MockDeployable runtime code hash 0x6ea525f6523fe148810254f04b9a74a379f59ca0f3a7fa83db90b691c78cc299
MockDeployableV2 derived address 0xDDB4cB0c3eaCE82Fbb15840CaeBDEc1A0Cdf98c3
MockDeployableV2 runtime code hash 0x62d58ae725f485b042d3fcac2842a3cd945a78e77b45b0f5d110cf6660c60311

diff of the before/after captures: byte-identical, all seven. Separately, git diff -M100% -U0 contains no hex literal on any changed line — the oracle literal 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854 in testDeployZoltu is untouched, and the five pre-existing named returns in src/lib/LibRainDeploy.sol are verified still present.

Tests: 30/30, nothing skipped, from a clean build

forge clean first, so the suite compiled from the moved paths with no stale artifacts:

Suite result: ok. 30 passed; 0 failed; 0 skipped; finished in 11.36s

Same 30 test names as the previous head. Fork tests ran live (Base archive binary search, live Arbitrum Zoltu factory) — none skipped.

Checks run locally (repo nix sol-shell)

check result
forge test (post-forge clean) 30 passed, 0 failed, 0 skipped
forge fmt --check clean, exit 0
slither . 4 contracts with 100 detectors, 0 result(s) found
reuse lint REUSE 3.3 compliant, 20/20 files (moved files kept their SPDX headers)

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

Reviewed 900a879: ready — derived addresses guard the stale-expected-address case the issue asked for; magic numbers derived to one irreducible fork-attested oracle; all three concrete mocks exact-pinned so the oracle's compiler is direct; named return dropped; tree matches the convention (mirror .t.sol by subject, mocks in test/concrete by own kind); every step carried a nothing-moved proof and 30/30 live-fork suite.

@thedavidmeister
thedavidmeister merged commit f5ee2e5 into main Jul 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ready AI vetter: passes review, ready for human decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant