From 1bbedc40433b8e00aef87d6537c471dac47d0c3d Mon Sep 17 00:00:00 2001 From: David Meister Date: Wed, 29 Jul 2026 13:22:17 +0000 Subject: [PATCH 1/7] fix(deploy): check the derived Zoltu address before skipping a network 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 --- README.md | 3 +- src/lib/LibRainDeploy.sol | 35 +++++++- test/src/lib/LibRainDeploy.t.sol | 128 ++++++++++++++++++++++++++++-- test/src/lib/MockDeployableV2.sol | 16 ++++ 4 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 test/src/lib/MockDeployableV2.sol diff --git a/README.md b/README.md index 6776143..9c2eb72 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ Approach: - Zoltu deterministic deployment proxy: same address on every supported network. - Caller-provided supported-network and dependency lists. - Hard guards against deploying to networks where dependencies are missing. -- Pre-calculated addresses asserted post-deploy: silent failures fail loudly. +- Pre-calculated addresses asserted against the creation code before deploying, + and against the chain after: silent failures fail loudly. - Bytecode integrity checks (e.g. via the Rain Extrospection lib) supported post-deploy. diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 962a2cb..56f8c51 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -147,6 +147,20 @@ library LibRainDeploy { vm.etch(ZOLTU_FACTORY, ZOLTU_FACTORY_BYTECODE); } + /// Derives the address the Zoltu factory deploys the given creation code + /// to. The factory is CREATE2 over its calldata with a zero salt, so the + /// address is a pure function of the creation code and is identical on + /// every network. + /// @param creationCode The creation code to derive the address for. + /// @return derivedAddress The address the creation code deploys to. + function zoltuAddress(bytes memory creationCode) internal pure returns (address derivedAddress) { + derivedAddress = address( + uint160( + uint256(keccak256(abi.encodePacked(bytes1(0xff), ZOLTU_FACTORY, bytes32(0), keccak256(creationCode)))) + ) + ); + } + /// Deploys the given creation code via the Zoltu factory. /// Handles the return data and errors appropriately. /// @param creationCode The creation code to deploy. @@ -185,6 +199,11 @@ library LibRainDeploy { } /// Deploys the given creation code to each network via the Zoltu factory. + /// `expectedAddress` MUST be the address the Zoltu factory derives for + /// `creationCode`, which is checked before any network is forked, so an + /// expected address that disagrees with the creation code fails loudly + /// rather than matching some other contract already deployed there and + /// skipping every network. /// For each network it forks once, verifies the Zoltu factory and every /// dependency have code (the factory codehash must also match), then /// broadcasts the deploy on that same fork. If code already exists at @@ -201,7 +220,8 @@ library LibRainDeploy { /// @param deployer The deployer address. /// @param creationCode The creation code to deploy. /// @param contractPath The contract path for verification commands. - /// @param expectedAddress The expected deterministic address. + /// @param expectedAddress The expected deterministic address, which MUST be + /// the address the Zoltu factory derives for `creationCode`. /// @param expectedCodeHash The expected code hash of the deployed contract. /// @param dependencies The addresses that must have code on each network. /// @return deployedAddress The deployed contract address. @@ -218,6 +238,16 @@ library LibRainDeploy { if (networks.length == 0) { revert NoNetworks(); } + // The Zoltu factory deploys the given creation code to a single + // deterministic address on every network, so an expected address that + // disagrees with the creation code can never hold that code. Checked + // up front, before any fork, because otherwise a network that already + // has some other contract at the expected address takes the skip + // branch and reports success without ever deploying. + address derivedAddress = zoltuAddress(creationCode); + if (derivedAddress != expectedAddress) { + revert UnexpectedDeployedAddress(expectedAddress, derivedAddress); + } for (uint256 i = 0; i < networks.length; i++) { // createSelectFork returns a fork id that is not needed here; bind // and reference it so the unused-return lint stays satisfied. @@ -282,7 +312,8 @@ library LibRainDeploy { /// @param deployerPrivateKey The private key to use for broadcasting. /// @param creationCode The creation code to deploy. /// @param contractPath The contract path for verification commands. - /// @param expectedAddress The expected deterministic address. + /// @param expectedAddress The expected deterministic address, which MUST be + /// the address the Zoltu factory derives for `creationCode`. /// @param expectedCodeHash The expected code hash of the deployed contract. /// @param dependencies The dependency addresses to check. /// @return deployedAddress The address of the deployed contract. diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index c08ec54..7981f3d 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; import {MockDeployable} from "./MockDeployable.sol"; +import {MockDeployableV2} from "./MockDeployableV2.sol"; import {MockReverter} from "./MockReverter.sol"; /// @title LibRainDeployTest @@ -212,7 +213,7 @@ contract LibRainDeployTest is Test { } /// `deployToNetworks` MUST revert with `NoNetworks` when given an empty - /// networks array. + /// networks array, before any other input is checked. function testDeployToNetworksNoNetworksReverts() external { string[] memory networks = new string[](0); address[] memory dependencies = new address[](0); @@ -298,7 +299,7 @@ contract LibRainDeployTest is Test { } /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the - /// deployed address does not match the expected address. + /// creation code does not deploy to the expected address. function testUnexpectedDeployedAddressReverts() external { string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; @@ -427,7 +428,15 @@ contract LibRainDeployTest is Test { LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, LibRainDeploy.ZOLTU_FACTORY ) ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + bytes32(0), + dependencies + ); } /// `deployToNetworks` MUST revert with `DependencyChanged` when the Zoltu @@ -451,7 +460,15 @@ contract LibRainDeployTest is Test { keccak256(hex"00") ) ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + bytes32(0), + dependencies + ); } /// `deployToNetworks` MUST revert with `MissingDependency` when a dependency @@ -467,6 +484,107 @@ contract LibRainDeployTest is Test { LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) ) ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + bytes32(0), + dependencies + ); + } + + /// `zoltuAddress` MUST derive the address the Zoltu factory actually + /// deploys the given creation code to, and creation code that differs MUST + /// derive a different address. + function testZoltuAddressMatchesFactoryDeploy() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + assertEq( + LibRainDeploy.zoltuAddress(type(MockDeployable).creationCode), + this.externalDeployZoltu(type(MockDeployable).creationCode) + ); + assertEq( + LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode), + this.externalDeployZoltu(type(MockDeployableV2).creationCode) + ); + assertNotEq( + LibRainDeploy.zoltuAddress(type(MockDeployable).creationCode), + LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode) + ); + } + + /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the + /// creation code does not deploy to `expectedAddress`, even when a contract + /// with the expected code hash already sits at that address on every + /// network and would otherwise be skipped as already deployed. + function testDeployToNetworksStaleExpectedAddressReverts() external { + vm.makePersistent(address(this)); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); + assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + vm.makePersistent(deployed); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedAddress.selector, + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B + ) + ); + // The new contract's creation code paired with the old contract's + // address and code hash. + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployableV2).creationCode, + "test/src/lib/MockDeployableV2.sol:MockDeployableV2", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + + // The new contract was not deployed anywhere. + assertEq(address(0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B).code.length, 0); + } + + /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the + /// factory reports an address other than the one derived from the creation + /// code, so the chain is checked and not only the derivation. + function testDeployToNetworksFactoryReportsOtherAddressReverts() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + // The factory reports the address of a contract that does have code, + // but not the one the creation code derives. + vm.mockCall( + LibRainDeploy.ZOLTU_FACTORY, + type(MockDeployable).creationCode, + abi.encodePacked(bytes20(LibRainDeploy.ZOLTU_FACTORY)) + ); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedAddress.selector, + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + LibRainDeploy.ZOLTU_FACTORY + ) + ); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/src/lib/MockDeployable.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); } } diff --git a/test/src/lib/MockDeployableV2.sol b/test/src/lib/MockDeployableV2.sol new file mode 100644 index 0000000..212565e --- /dev/null +++ b/test/src/lib/MockDeployableV2.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +/// @title MockDeployableV2 +/// Deployment target whose creation code differs from `MockDeployable`, so the +/// two derive different Zoltu addresses. Stands in for a contract whose +/// bytecode has changed while its pinned constants have not. +contract MockDeployableV2 { + /// @notice Placeholder value to ensure the contract has non-trivial code. + uint256 public value = 43; + + /// @notice Second placeholder value so the runtime code also differs from + /// `MockDeployable`. + uint256 public other = 99; +} From ee1a7c6201c5a5b4478be4ff139d1625951d79e3 Mon Sep 17 00:00:00 2001 From: David Meister Date: Wed, 29 Jul 2026 13:36:28 +0000 Subject: [PATCH 2/7] test(deploy): pin the derived-address check to before any fork 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 --- test/src/lib/LibRainDeploy.t.sol | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 7981f3d..ba85ccd 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -553,6 +553,27 @@ contract LibRainDeployTest is Test { assertEq(address(0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B).code.length, 0); } + /// `deployToNetworks` MUST check `expectedAddress` against the creation + /// code before it forks anything, so the mismatch is reported without any + /// network being reachable at all. + function testDeployToNetworksStaleExpectedAddressRevertsBeforeForking() external { + string[] memory networks = new string[](1); + // Not a configured RPC alias, so forking it is itself an error. + networks[0] = "unconfigured_network"; + address[] memory dependencies = new address[](0); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedAddress.selector, + address(0xdead), + 0xC24016f209562fc151e5Ab7F88694ED5775feb36 + ) + ); + this.externalDeployToNetworks( + networks, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0), dependencies + ); + } + /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the /// factory reports an address other than the one derived from the creation /// code, so the chain is checked and not only the derivation. From 16958621cee97950a8565525b6467257521a184c Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 30 Jul 2026 09:40:50 +0000 Subject: [PATCH 3/7] test(deploy): derive the mock Zoltu addresses and code hash `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) --- test/src/lib/LibRainDeploy.t.sol | 95 +++++++++++++++++++------------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index ba85ccd..df9f422 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -13,6 +13,30 @@ import {MockReverter} from "./MockReverter.sol"; /// that need `vm.expectRevert` at the correct call depth, and for functions /// that require a storage mapping reference. contract LibRainDeployTest is Test { + /// The address the Zoltu factory deploys `MockDeployable` to. Derived from + /// the mock's creation code by the same formula the factory applies, so it + /// follows the compiler that builds the mock. `testDeployZoltu` pins the + /// derivation against the live factory on a fork. + /// @return The deterministic address for `MockDeployable`. + function mockDeployableAddress() internal pure returns (address) { + return LibRainDeploy.zoltuAddress(type(MockDeployable).creationCode); + } + + /// The code hash `MockDeployable` has once deployed, i.e. `keccak256` over + /// the runtime code its creation code leaves behind. Derived from the mock + /// rather than pinned, for the same reason as `mockDeployableAddress`. + /// @return The deployed code hash for `MockDeployable`. + function mockDeployableCodeHash() internal pure returns (bytes32) { + return keccak256(type(MockDeployable).runtimeCode); + } + + /// The address the Zoltu factory deploys `MockDeployableV2` to, derived the + /// same way as `mockDeployableAddress`. + /// @return The deterministic address for `MockDeployableV2`. + function mockDeployableV2Address() internal pure returns (address) { + return LibRainDeploy.zoltuAddress(type(MockDeployableV2).creationCode); + } + /// External wrapper for `isStartBlock` so that it can be called /// externally in tests. /// @param target The contract address to check. @@ -235,11 +259,11 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); - assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(result, mockDeployableAddress()); } /// External wrapper for `deployToNetworks` so that `vm.expectRevert` @@ -278,6 +302,9 @@ contract LibRainDeployTest is Test { function testDeployZoltu() external { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); + // Pinned literal, deliberately not `mockDeployableAddress()`. The live + // factory on the fork is the oracle here, so an expected value taken + // from the derivation would only check `zoltuAddress` against itself. assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } @@ -306,9 +333,7 @@ contract LibRainDeployTest is Test { address[] memory dependencies = new address[](0); vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedAddress.selector, - address(0xdead), - 0xC24016f209562fc151e5Ab7F88694ED5775feb36 + LibRainDeploy.UnexpectedDeployedAddress.selector, address(0xdead), mockDeployableAddress() ) ); this.externalDeployToNetworks( @@ -322,13 +347,11 @@ contract LibRainDeployTest is Test { string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; address[] memory dependencies = new address[](0); - address expectedAddress = 0xC24016f209562fc151e5Ab7F88694ED5775feb36; + address expectedAddress = mockDeployableAddress(); bytes32 wrongCodeHash = bytes32(uint256(1)); vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedCodeHash.selector, - wrongCodeHash, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483 + LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongCodeHash, mockDeployableCodeHash() ) ); this.externalDeployToNetworks( @@ -347,11 +370,11 @@ contract LibRainDeployTest is Test { 1, type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); - assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(deployed, mockDeployableAddress()); } /// `deployToNetworks` MUST skip deployment and return the expected address @@ -361,7 +384,7 @@ contract LibRainDeployTest is Test { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); - assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(deployed, mockDeployableAddress()); vm.makePersistent(deployed); string[] memory networks = new string[](1); @@ -373,11 +396,11 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); - assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(result, mockDeployableAddress()); } /// `deployToNetworks` MUST skip an already-deployed network WITHOUT checking @@ -389,7 +412,7 @@ contract LibRainDeployTest is Test { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); - assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(deployed, mockDeployableAddress()); vm.makePersistent(deployed); string[] memory networks = new string[](1); @@ -404,11 +427,11 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); - assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(result, mockDeployableAddress()); } /// `deployToNetworks` MUST revert with `MissingDependency` when the Zoltu @@ -433,7 +456,7 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + mockDeployableAddress(), bytes32(0), dependencies ); @@ -465,7 +488,7 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + mockDeployableAddress(), bytes32(0), dependencies ); @@ -489,7 +512,7 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + mockDeployableAddress(), bytes32(0), dependencies ); @@ -523,7 +546,7 @@ contract LibRainDeployTest is Test { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); - assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(deployed, mockDeployableAddress()); vm.makePersistent(deployed); string[] memory networks = new string[](1); @@ -532,9 +555,7 @@ contract LibRainDeployTest is Test { vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedAddress.selector, - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B + LibRainDeploy.UnexpectedDeployedAddress.selector, mockDeployableAddress(), mockDeployableV2Address() ) ); // The new contract's creation code paired with the old contract's @@ -544,13 +565,13 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployableV2).creationCode, "test/src/lib/MockDeployableV2.sol:MockDeployableV2", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); // The new contract was not deployed anywhere. - assertEq(address(0x1efA03dD8f7D8e86Bbd2eEBe25f63052e95C002B).code.length, 0); + assertEq(mockDeployableV2Address().code.length, 0); } /// `deployToNetworks` MUST check `expectedAddress` against the creation @@ -564,9 +585,7 @@ contract LibRainDeployTest is Test { vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedAddress.selector, - address(0xdead), - 0xC24016f209562fc151e5Ab7F88694ED5775feb36 + LibRainDeploy.UnexpectedDeployedAddress.selector, address(0xdead), mockDeployableAddress() ) ); this.externalDeployToNetworks( @@ -593,9 +612,7 @@ contract LibRainDeployTest is Test { vm.expectRevert( abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedAddress.selector, - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - LibRainDeploy.ZOLTU_FACTORY + LibRainDeploy.UnexpectedDeployedAddress.selector, mockDeployableAddress(), LibRainDeploy.ZOLTU_FACTORY ) ); this.externalDeployToNetworks( @@ -603,8 +620,8 @@ contract LibRainDeployTest is Test { address(this), type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", - 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + mockDeployableAddress(), + mockDeployableCodeHash(), dependencies ); } From fb137c6654d6a35e7642701f99934b0215daa7ba Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 30 Jul 2026 09:42:35 +0000 Subject: [PATCH 4/7] fix(deploy): pin MockDeployableV2 to an exact compiler version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- test/src/lib/LibRainDeploy.t.sol | 5 ++++- test/src/lib/MockDeployableV2.sol | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index df9f422..4800355 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -305,7 +305,10 @@ contract LibRainDeployTest is Test { // Pinned literal, deliberately not `mockDeployableAddress()`. The live // factory on the fork is the oracle here, so an expected value taken // from the derivation would only check `zoltuAddress` against itself. - assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + // 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`. + assertEq(deployed, 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854); } /// `deployZoltu` MUST revert with `DeployFailed` when the Zoltu factory diff --git a/test/src/lib/MockDeployableV2.sol b/test/src/lib/MockDeployableV2.sol index 212565e..8c92819 100644 --- a/test/src/lib/MockDeployableV2.sol +++ b/test/src/lib/MockDeployableV2.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-DCL-1.0 // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; +pragma solidity =0.8.25; /// @title MockDeployableV2 /// Deployment target whose creation code differs from `MockDeployable`, so the From 528a7c7cf2ad0b6cd65ece23d567fd397f831192 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 30 Jul 2026 10:14:43 +0000 Subject: [PATCH 5/7] style(deploy): drop the named return from zoltuAddress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- src/lib/LibRainDeploy.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 56f8c51..39c5175 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -152,9 +152,9 @@ library LibRainDeploy { /// address is a pure function of the creation code and is identical on /// every network. /// @param creationCode The creation code to derive the address for. - /// @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( uint160( uint256(keccak256(abi.encodePacked(bytes1(0xff), ZOLTU_FACTORY, bytes32(0), keccak256(creationCode)))) ) From 2b685a9b30f5c2e57f19bb069b18bcb6a9241a8d Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 30 Jul 2026 12:28:09 +0000 Subject: [PATCH 6/7] fix(deploy): pin MockDeployable and MockReverter to an exact compiler 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) --- test/src/lib/LibRainDeploy.t.sol | 3 +-- test/src/lib/MockDeployable.sol | 2 +- test/src/lib/MockReverter.sol | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 4800355..8320189 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -306,8 +306,7 @@ contract LibRainDeployTest is Test { // factory on the fork is the oracle here, so an expected value taken // from the derivation would only check `zoltuAddress` against itself. // 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`. assertEq(deployed, 0x1fa1bBf9Cf73B1aCCc1a3D9de5896E81Cd567854); } diff --git a/test/src/lib/MockDeployable.sol b/test/src/lib/MockDeployable.sol index 2309443..06790a8 100644 --- a/test/src/lib/MockDeployable.sol +++ b/test/src/lib/MockDeployable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-DCL-1.0 // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; +pragma solidity =0.8.25; /// @title MockDeployable /// Minimal contract used as a deployment target for Zoltu factory tests. diff --git a/test/src/lib/MockReverter.sol b/test/src/lib/MockReverter.sol index 32bb09d..2081d63 100644 --- a/test/src/lib/MockReverter.sol +++ b/test/src/lib/MockReverter.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: LicenseRef-DCL-1.0 // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.25; +pragma solidity =0.8.25; /// @title MockReverter /// Contract whose constructor always reverts, used to test DeployFailed with From 900a879cef164b64279c117b3922c3179efb3093 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 30 Jul 2026 15:19:53 +0000 Subject: [PATCH 7/7] refactor(test): move the concrete mocks out of the mirror tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- test/{src/lib => concrete}/MockDeployable.sol | 0 .../{src/lib => concrete}/MockDeployableV2.sol | 0 test/{src/lib => concrete}/MockReverter.sol | 0 test/src/lib/LibRainDeploy.t.sol | 18 +++++++++--------- 4 files changed, 9 insertions(+), 9 deletions(-) rename test/{src/lib => concrete}/MockDeployable.sol (100%) rename test/{src/lib => concrete}/MockDeployableV2.sol (100%) rename test/{src/lib => concrete}/MockReverter.sol (100%) diff --git a/test/src/lib/MockDeployable.sol b/test/concrete/MockDeployable.sol similarity index 100% rename from test/src/lib/MockDeployable.sol rename to test/concrete/MockDeployable.sol diff --git a/test/src/lib/MockDeployableV2.sol b/test/concrete/MockDeployableV2.sol similarity index 100% rename from test/src/lib/MockDeployableV2.sol rename to test/concrete/MockDeployableV2.sol diff --git a/test/src/lib/MockReverter.sol b/test/concrete/MockReverter.sol similarity index 100% rename from test/src/lib/MockReverter.sol rename to test/concrete/MockReverter.sol diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 8320189..4991305 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -4,9 +4,9 @@ pragma solidity ^0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; -import {MockDeployable} from "./MockDeployable.sol"; -import {MockDeployableV2} from "./MockDeployableV2.sol"; -import {MockReverter} from "./MockReverter.sol"; +import {MockDeployable} from "../../concrete/MockDeployable.sol"; +import {MockDeployableV2} from "../../concrete/MockDeployableV2.sol"; +import {MockReverter} from "../../concrete/MockReverter.sol"; /// @title LibRainDeployTest /// Tests for `LibRainDeploy`. External wrappers are used for library functions @@ -258,7 +258,7 @@ contract LibRainDeployTest is Test { networks, address(this), type(MockDeployable).creationCode, - "test/src/lib/MockDeployable.sol:MockDeployable", + "test/concrete/MockDeployable.sol:MockDeployable", mockDeployableAddress(), mockDeployableCodeHash(), dependencies @@ -371,7 +371,7 @@ contract LibRainDeployTest is Test { networks, 1, type(MockDeployable).creationCode, - "test/src/lib/MockDeployable.sol:MockDeployable", + "test/concrete/MockDeployable.sol:MockDeployable", mockDeployableAddress(), mockDeployableCodeHash(), dependencies @@ -397,7 +397,7 @@ contract LibRainDeployTest is Test { networks, address(this), type(MockDeployable).creationCode, - "test/src/lib/MockDeployable.sol:MockDeployable", + "test/concrete/MockDeployable.sol:MockDeployable", mockDeployableAddress(), mockDeployableCodeHash(), dependencies @@ -428,7 +428,7 @@ contract LibRainDeployTest is Test { networks, address(this), type(MockDeployable).creationCode, - "test/src/lib/MockDeployable.sol:MockDeployable", + "test/concrete/MockDeployable.sol:MockDeployable", mockDeployableAddress(), mockDeployableCodeHash(), dependencies @@ -566,7 +566,7 @@ contract LibRainDeployTest is Test { networks, address(this), type(MockDeployableV2).creationCode, - "test/src/lib/MockDeployableV2.sol:MockDeployableV2", + "test/concrete/MockDeployableV2.sol:MockDeployableV2", mockDeployableAddress(), mockDeployableCodeHash(), dependencies @@ -621,7 +621,7 @@ contract LibRainDeployTest is Test { networks, address(this), type(MockDeployable).creationCode, - "test/src/lib/MockDeployable.sol:MockDeployable", + "test/concrete/MockDeployable.sol:MockDeployable", mockDeployableAddress(), mockDeployableCodeHash(), dependencies