From 5bfe2c495e9e2af749d3ec304b498104dfc88f4f Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 00:32:09 +0000 Subject: [PATCH 1/5] fix(deploy): validate dependencies once on the reused fork deployAndBroadcast ran two phases that each forked every network: checkDependencies forked each network to validate the Zoltu factory and dependencies, then deployToNetworks forked each network again at a newer head, re-read the same dependencies, and broadcast the deploy. That second fork's re-read on a newer snapshot let a transient RPC inconsistency report an already-deployed dependency as having no code, falsely reverting MissingDependency and aborting a valid deploy. checkDependencies now returns the fork it created and selected for each network, and deployToNetworks reuses that fork via vm.selectFork and deploys. Validating once on the fork the deploy runs on removes the redundant second read, so there is no longer a divergent snapshot for a transient glitch to surface on. The deploy-time re-check and the dependency codehash record/compare machinery (meaningful only across two forks) are removed, and deployAndBroadcast no longer needs the depCodeHashes mapping parameter. Adds a fork-reuse regression test and a two-network per-index fork-selection test; removes the tests that exercised the deleted deploy-time re-check and codehash recording. Co-Authored-By: Claude Opus 4.8 --- src/lib/LibRainDeploy.sol | 89 ++++-------- test/src/lib/LibRainDeploy.t.sol | 231 ++++++++++++------------------- 2 files changed, 120 insertions(+), 200 deletions(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index d63e64f..319fd87 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -185,25 +185,24 @@ library LibRainDeploy { } /// Checks that the Zoltu factory has the expected codehash and all - /// dependencies have code on each network. Records each dependency's - /// codehash in the provided mapping. + /// dependencies have code on each network, returning the fork validated for + /// each network so the deploy phase can reuse it. /// @param vm The Vm instance to use for forking. /// @param networks The list of network names to check. /// @param dependencies The addresses that must have code on each network. - /// @param depCodeHashes Storage mapping to record dependency codehashes. - function checkDependencies( - Vm vm, - string[] memory networks, - address[] memory dependencies, - mapping(string => mapping(address => bytes32)) storage depCodeHashes - ) internal { + /// @return forkIds The fork id created and selected for each network, in the + /// same order as `networks`, so the deploy phase can reuse the validated + /// fork instead of creating a fresh one. + function checkDependencies(Vm vm, string[] memory networks, address[] memory dependencies) + internal + returns (uint256[] memory forkIds) + { if (networks.length == 0) { revert NoNetworks(); } + forkIds = new uint256[](networks.length); for (uint256 i = 0; i < networks.length; i++) { - // Capture return value to suppress slither unused-return warning. - uint256 forkId = vm.createSelectFork(networks[i]); - (forkId); + forkIds[i] = vm.createSelectFork(networks[i]); console2.log("Block number:", block.number); console2.log("Checking dependencies on network:", networks[i]); @@ -221,69 +220,47 @@ library LibRainDeploy { if (dependencies[j].code.length == 0) { revert MissingDependency(networks[i], dependencies[j]); } - depCodeHashes[networks[i]][dependencies[j]] = dependencies[j].codehash; } } } - /// Verifies that dependencies have not changed since the check phase, - /// then deploys to each network via the Zoltu factory. If code already - /// exists at `expectedAddress`, deployment is skipped for that network. + /// Deploys to each network via the Zoltu factory, reusing the fork that + /// `checkDependencies` already validated. If code already exists at + /// `expectedAddress`, deployment is skipped for that network. /// @param vm The Vm instance to use for forking and broadcasting. /// @param networks The list of network names to deploy to. + /// @param forkIds The fork ids returned by `checkDependencies`, reused per + /// network so the deploy runs on the same validated snapshot. /// @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 expectedCodeHash The expected code hash of the deployed contract. - /// @param dependencies The dependency addresses to re-verify. - /// @param depCodeHashes Storage mapping of recorded dependency codehashes. /// @return deployedAddress The deployed contract address. function deployToNetworks( Vm vm, string[] memory networks, + uint256[] memory forkIds, address deployer, bytes memory creationCode, string memory contractPath, address expectedAddress, - bytes32 expectedCodeHash, - address[] memory dependencies, - mapping(string => mapping(address => bytes32)) storage depCodeHashes + bytes32 expectedCodeHash ) internal returns (address deployedAddress) { if (networks.length == 0) { revert NoNetworks(); } for (uint256 i = 0; i < networks.length; i++) { console2.log("Deploying to network:", networks[i]); - // Capture return value to suppress slither unused-return warning. - uint256 forkId = vm.createSelectFork(networks[i]); - (forkId); + // Reuse the fork that checkDependencies already validated for this + // network, rather than creating a fresh fork at a newer head. The + // dependencies were verified on this exact snapshot; a fresh fork + // re-reads them on a different one, where a transient RPC + // inconsistency can report an already-deployed dependency as missing + // and abort an otherwise-valid deploy. + vm.selectFork(forkIds[i]); console2.log("Block number:", block.number); - // Re-verify Zoltu factory exists. - if (ZOLTU_FACTORY.code.length == 0) { - revert MissingDependency(networks[i], ZOLTU_FACTORY); - } - // Re-verify Zoltu factory codehash. - if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { - revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); - } - - // Re-verify dependencies have not changed since the check phase. - for (uint256 j = 0; j < dependencies.length; j++) { - if (dependencies[j].code.length == 0) { - revert MissingDependency(networks[i], dependencies[j]); - } - if (dependencies[j].codehash != depCodeHashes[networks[i]][dependencies[j]]) { - revert DependencyChanged( - networks[i], - dependencies[j], - depCodeHashes[networks[i]][dependencies[j]], - dependencies[j].codehash - ); - } - } - vm.startBroadcast(deployer); if (expectedAddress.code.length == 0) { console2.log(" - Deploying via Zoltu"); @@ -322,7 +299,6 @@ library LibRainDeploy { /// @param expectedAddress The expected deterministic address. /// @param expectedCodeHash The expected code hash of the deployed contract. /// @param dependencies The dependency addresses to check. - /// @param depCodeHashes Storage mapping to record dependency codehashes. /// @return deployedAddress The address of the deployed contract. function deployAndBroadcast( Vm vm, @@ -332,8 +308,7 @@ library LibRainDeploy { string memory contractPath, address expectedAddress, bytes32 expectedCodeHash, - address[] memory dependencies, - mapping(string => mapping(address => bytes32)) storage depCodeHashes + address[] memory dependencies ) internal returns (address deployedAddress) { if (networks.length == 0) { revert NoNetworks(); @@ -342,17 +317,9 @@ library LibRainDeploy { console2.log("Deploying from address:", deployer); - checkDependencies(vm, networks, dependencies, depCodeHashes); + uint256[] memory forkIds = checkDependencies(vm, networks, dependencies); deployedAddress = deployToNetworks( - vm, - networks, - deployer, - creationCode, - contractPath, - expectedAddress, - expectedCodeHash, - dependencies, - depCodeHashes + vm, networks, forkIds, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash ); } } diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 8a39f30..781970c 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -26,8 +26,6 @@ contract MockReverter { /// that need `vm.expectRevert` at the correct call depth, and for functions /// that require a storage mapping reference. contract LibRainDeployTest is Test { - mapping(string => mapping(address => bytes32)) internal sDepCodeHashes; - /// External wrapper for `isStartBlock` so that it can be called /// externally in tests. /// @param target The contract address to check. @@ -215,8 +213,7 @@ contract LibRainDeployTest is Test { contractPath, expectedAddress, expectedCodeHash, - dependencies, - sDepCodeHashes + dependencies ); } @@ -241,17 +238,88 @@ contract LibRainDeployTest is Test { /// networks array. function testDeployToNetworksNoNetworksReverts() external { string[] memory networks = new string[](0); - address[] memory dependencies = new address[](0); + uint256[] memory forkIds = new uint256[](0); vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); + this.externalDeployToNetworks(networks, forkIds, address(this), hex"", "", address(0), bytes32(0)); + } + + /// `deployToNetworks` MUST reuse the fork that `checkDependencies` validated + /// for each network, never create a fresh one. A fresh fork re-reads the + /// dependencies on a newer snapshot, where a transient RPC inconsistency can + /// report an already-deployed dependency as missing and abort a valid + /// deploy. After `deployToNetworks` returns, the active fork MUST equal the + /// fork id `checkDependencies` returned for the last network, proving the + /// validated fork was reused rather than replaced (a fresh fork would leave + /// a different active fork id). + function testDeployToNetworksReusesValidatedFork() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.BASE; + address[] memory dependencies = new address[](1); + dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; + + uint256[] memory forkIds = this.externalCheckDependencies(networks, dependencies); + assertEq(forkIds.length, networks.length); + + // Target the already-deployed Zoltu factory so the deploy is skipped; + // this test asserts fork reuse, not the deployment itself. + this.externalDeployToNetworks( + networks, + forkIds, + address(this), + hex"", + "", + LibRainDeploy.ZOLTU_FACTORY, + LibRainDeploy.ZOLTU_FACTORY_CODEHASH + ); + + assertEq(vm.activeFork(), forkIds[networks.length - 1]); + } + + /// With multiple networks, `deployToNetworks` MUST select each network's own + /// validated fork by index (`forkIds[i]`), not a fixed one. Two distinct + /// networks produce two distinct fork ids; after deploying to both, the + /// active fork MUST be the SECOND network's fork, which only holds if the + /// per-index selection is correct (selecting `forkIds[0]` for every network + /// would leave the first network's fork active instead). + function testDeployToNetworksReusesPerNetworkFork() external { + string[] memory networks = new string[](2); + networks[0] = LibRainDeploy.BASE; + networks[1] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; + + uint256[] memory forkIds = this.externalCheckDependencies(networks, dependencies); + assertEq(forkIds.length, networks.length); + // Each network gets its own fork. + assertTrue(forkIds[0] != forkIds[1]); + + // Target the already-deployed Zoltu factory so the deploy is skipped on + // both networks; this test asserts per-network fork selection. + this.externalDeployToNetworks( + networks, + forkIds, + address(this), + hex"", + "", + LibRainDeploy.ZOLTU_FACTORY, + LibRainDeploy.ZOLTU_FACTORY_CODEHASH + ); + + // The loop ends on the last network, so the active fork must be the last + // network's fork. Selecting forkIds[0] for every iteration would leave + // forkIds[0] active here, killing that mutation. + assertEq(vm.activeFork(), forkIds[1]); } /// External wrapper for `checkDependencies` so that `vm.expectRevert` /// works at the correct call depth. /// @param networks The list of network names to check. /// @param dependencies The dependency addresses to check. - function externalCheckDependencies(string[] memory networks, address[] memory dependencies) external { - LibRainDeploy.checkDependencies(vm, networks, dependencies, sDepCodeHashes); + function externalCheckDependencies(string[] memory networks, address[] memory dependencies) + external + returns (uint256[] memory forkIds) + { + forkIds = LibRainDeploy.checkDependencies(vm, networks, dependencies); } /// External wrapper for `deployToNetworks` so that `vm.expectRevert` @@ -262,27 +330,18 @@ contract LibRainDeployTest is Test { /// @param contractPath The contract path for verification commands. /// @param expectedAddress The expected deterministic address. /// @param expectedCodeHash The expected code hash of the deployed contract. - /// @param dependencies The dependency addresses to re-verify. /// @return deployedAddress The deployed contract address. function externalDeployToNetworks( string[] memory networks, + uint256[] memory forkIds, address deployer, bytes memory creationCode, string memory contractPath, address expectedAddress, - bytes32 expectedCodeHash, - address[] memory dependencies + bytes32 expectedCodeHash ) external returns (address deployedAddress) { deployedAddress = LibRainDeploy.deployToNetworks( - vm, - networks, - deployer, - creationCode, - contractPath, - expectedAddress, - expectedCodeHash, - dependencies, - sDepCodeHashes + vm, networks, forkIds, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash ); } @@ -321,10 +380,11 @@ contract LibRainDeployTest is Test { /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the /// deployed address does not match the expected address. function testUnexpectedDeployedAddressReverts() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - address[] memory dependencies = new address[](0); + uint256 forkId = vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; + uint256[] memory forkIds = new uint256[](1); + forkIds[0] = forkId; vm.expectRevert( abi.encodeWithSelector( LibRainDeploy.UnexpectedDeployedAddress.selector, @@ -333,17 +393,18 @@ contract LibRainDeployTest is Test { ) ); this.externalDeployToNetworks( - networks, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0), dependencies + networks, forkIds, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0) ); } /// `deployToNetworks` MUST revert with `UnexpectedDeployedCodeHash` when the /// deployed code hash does not match the expected code hash. function testUnexpectedDeployedCodeHashReverts() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - address[] memory dependencies = new address[](0); + uint256 forkId = vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; + uint256[] memory forkIds = new uint256[](1); + forkIds[0] = forkId; address expectedAddress = 0xC24016f209562fc151e5Ab7F88694ED5775feb36; bytes32 wrongCodeHash = bytes32(uint256(1)); vm.expectRevert( @@ -354,7 +415,7 @@ contract LibRainDeployTest is Test { ) ); this.externalDeployToNetworks( - networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies + networks, forkIds, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash ); } @@ -381,40 +442,28 @@ contract LibRainDeployTest is Test { function testDeployToNetworksSkipsWhenAlreadyDeployed() external { vm.makePersistent(address(this)); - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + uint256 forkId = 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); + uint256[] memory forkIds = new uint256[](1); + forkIds[0] = forkId; address result = this.externalDeployToNetworks( networks, + forkIds, address(this), type(MockDeployable).creationCode, "test/src/lib/LibRainDeploy.t.sol:MockDeployable", 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, - dependencies + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483 ); assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } - /// `checkDependencies` MUST record the codehash of each dependency in the - /// storage mapping after verifying it exists. - function testCheckDependenciesRecordsCodehash() external { - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](1); - dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; - - this.externalCheckDependencies(networks, dependencies); - - assertTrue(sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] != bytes32(0)); - } - /// `checkDependencies` MUST revert with `MissingDependency` when the /// Zoltu factory has no code on the network. function testCheckDependenciesMissingZoltuFactoryReverts() external { @@ -472,100 +521,4 @@ contract LibRainDeployTest is Test { ); this.externalCheckDependencies(networks, dependencies); } - - /// `deployToNetworks` MUST revert with `MissingDependency` when the - /// Zoltu factory has been removed since the check phase. - function testDeployToNetworksZoltuFactoryMissingReverts() external { - vm.makePersistent(address(this)); - - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); - vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, LibRainDeploy.ZOLTU_FACTORY - ) - ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); - } - - /// `deployToNetworks` MUST revert with `DependencyChanged` when the - /// Zoltu factory has a wrong codehash at deploy time. - function testDeployToNetworksZoltuFactoryCodehashChangedReverts() external { - vm.makePersistent(address(this)); - - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); - vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex"00"); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.DependencyChanged.selector, - LibRainDeploy.ARBITRUM_ONE, - LibRainDeploy.ZOLTU_FACTORY, - LibRainDeploy.ZOLTU_FACTORY_CODEHASH, - keccak256(hex"00") - ) - ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); - } - - /// `deployToNetworks` MUST revert with `DependencyChanged` when a - /// dependency's codehash differs from what was recorded during the check - /// phase. - function testDependencyChangedCodehashReverts() external { - // Make the test contract persistent so storage survives fork switches. - vm.makePersistent(address(this)); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](1); - dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; - - // Pre-populate with a wrong codehash to simulate a change between - // the check and deploy phases. - sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][LibRainDeploy.ZOLTU_FACTORY] = bytes32(uint256(1)); - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.DependencyChanged.selector, - LibRainDeploy.ARBITRUM_ONE, - LibRainDeploy.ZOLTU_FACTORY, - bytes32(uint256(1)), - LibRainDeploy.ZOLTU_FACTORY_CODEHASH - ) - ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); - } - - /// `deployToNetworks` MUST revert with `MissingDependency` when a - /// dependency has been destroyed (code.length == 0) since the check phase. - function testDependencyMissingAtDeployTimeReverts() external { - // Make the test contract persistent so storage survives fork switches. - vm.makePersistent(address(this)); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](1); - dependencies[0] = address(0xdead); - - // Pre-populate as if the dependency existed during the check phase. - sDepCodeHashes[LibRainDeploy.ARBITRUM_ONE][address(0xdead)] = bytes32(uint256(1)); - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) - ) - ); - this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); - } } From 4026da06e8793d0e770f574acc35bf2a16a73817 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 00:50:32 +0000 Subject: [PATCH 2/5] test(deploy): split mock contracts into their own files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-contract static check (rainix-sol-single-contract) requires one contract per .sol file. Move MockDeployable and MockReverter out of LibRainDeploy.t.sol into their own files and import them. bytecode_hash is "none", so the mocks' creation code — and the deterministic Zoltu addresses the tests assert — are unchanged. Co-Authored-By: Claude Opus 4.8 --- test/src/lib/LibRainDeploy.t.sol | 22 ++++------------------ test/src/lib/MockDeployable.sol | 10 ++++++++++ test/src/lib/MockReverter.sol | 12 ++++++++++++ 3 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 test/src/lib/MockDeployable.sol create mode 100644 test/src/lib/MockReverter.sol diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 781970c..09f1885 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -4,22 +4,8 @@ pragma solidity ^0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; - -/// @title MockDeployable -/// Minimal contract used as a deployment target for Zoltu factory tests. -contract MockDeployable { - /// @notice Placeholder value to ensure the contract has non-trivial code. - uint256 public value = 42; -} - -/// @title MockReverter -/// Contract whose constructor always reverts, used to test DeployFailed with -/// success=false. -contract MockReverter { - constructor() { - revert(); - } -} +import {MockDeployable} from "./MockDeployable.sol"; +import {MockReverter} from "./MockReverter.sol"; /// @title LibRainDeployTest /// Tests for `LibRainDeploy`. External wrappers are used for library functions @@ -429,7 +415,7 @@ contract LibRainDeployTest is Test { networks, 1, type(MockDeployable).creationCode, - "test/src/lib/LibRainDeploy.t.sol:MockDeployable", + "test/src/lib/MockDeployable.sol:MockDeployable", 0xC24016f209562fc151e5Ab7F88694ED5775feb36, 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, dependencies @@ -457,7 +443,7 @@ contract LibRainDeployTest is Test { forkIds, address(this), type(MockDeployable).creationCode, - "test/src/lib/LibRainDeploy.t.sol:MockDeployable", + "test/src/lib/MockDeployable.sol:MockDeployable", 0xC24016f209562fc151e5Ab7F88694ED5775feb36, 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483 ); diff --git a/test/src/lib/MockDeployable.sol b/test/src/lib/MockDeployable.sol new file mode 100644 index 0000000..2309443 --- /dev/null +++ b/test/src/lib/MockDeployable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +/// @title MockDeployable +/// Minimal contract used as a deployment target for Zoltu factory tests. +contract MockDeployable { + /// @notice Placeholder value to ensure the contract has non-trivial code. + uint256 public value = 42; +} diff --git a/test/src/lib/MockReverter.sol b/test/src/lib/MockReverter.sol new file mode 100644 index 0000000..32bb09d --- /dev/null +++ b/test/src/lib/MockReverter.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +/// @title MockReverter +/// Contract whose constructor always reverts, used to test DeployFailed with +/// success=false. +contract MockReverter { + constructor() { + revert(); + } +} From 503002eda94273c52e2029170d994f43172f9b84 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 10:39:48 +0000 Subject: [PATCH 3/5] refactor(deploy): fold the dependency check into deployToNetworks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deployAndBroadcast ran a separate checkDependencies pass over every network before deployToNetworks deployed to any of them. With the fork-reuse fix that pre-flight had become a second function that protected nothing: the Zoltu deploy is idempotent (an already-deployed contract is skipped) and each network is independent, so a failure on one network leaves the others intact and the script can simply be re-run. There is nothing for an all-network pre-flight to guard. deployToNetworks now forks each network once, verifies the Zoltu factory and every dependency have code on that fork, then broadcasts the deploy — all on a single fork per network, so each dependency is read exactly once. This removes the redundant second fork (the original transient-glitch bug) at the root and drops checkDependencies, the forkIds plumbing, and the two-pass structure entirely. The per-network dependency check still runs before that network's deploy, so nothing deploys onto a network missing a prerequisite. deployToNetworks regains the dependencies parameter and loses forkIds; deployAndBroadcast is otherwise unchanged. The dependency and Zoltu-factory checks now run through deployToNetworks (the persistent-etch test pattern survives its internal createSelectFork) and are mutation-validated; the fork-reuse and checkDependencies tests are removed and a two-network deploy test is added. Co-Authored-By: Claude Opus 4.8 --- src/lib/LibRainDeploy.sol | 90 +++++++------------ test/src/lib/LibRainDeploy.t.sol | 149 +++++++++---------------------- 2 files changed, 74 insertions(+), 165 deletions(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 319fd87..17f21de 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -184,83 +184,62 @@ library LibRainDeploy { return networks; } - /// Checks that the Zoltu factory has the expected codehash and all - /// dependencies have code on each network, returning the fork validated for - /// each network so the deploy phase can reuse it. - /// @param vm The Vm instance to use for forking. - /// @param networks The list of network names to check. - /// @param dependencies The addresses that must have code on each network. - /// @return forkIds The fork id created and selected for each network, in the - /// same order as `networks`, so the deploy phase can reuse the validated - /// fork instead of creating a fresh one. - function checkDependencies(Vm vm, string[] memory networks, address[] memory dependencies) - internal - returns (uint256[] memory forkIds) - { - if (networks.length == 0) { - revert NoNetworks(); - } - forkIds = new uint256[](networks.length); - for (uint256 i = 0; i < networks.length; i++) { - forkIds[i] = vm.createSelectFork(networks[i]); - console2.log("Block number:", block.number); - console2.log("Checking dependencies on network:", networks[i]); - - console2.log(" - Zoltu Factory:", ZOLTU_FACTORY); - // Zoltu factory must exist with the expected codehash. - if (ZOLTU_FACTORY.code.length == 0) { - revert MissingDependency(networks[i], ZOLTU_FACTORY); - } - if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { - revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); - } - - for (uint256 j = 0; j < dependencies.length; j++) { - console2.log(" - Dependency:", dependencies[j]); - if (dependencies[j].code.length == 0) { - revert MissingDependency(networks[i], dependencies[j]); - } - } - } - } - - /// Deploys to each network via the Zoltu factory, reusing the fork that - /// `checkDependencies` already validated. If code already exists at - /// `expectedAddress`, deployment is skipped for that network. + /// Deploys the given creation code to each network via the Zoltu factory. + /// 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 + /// `expectedAddress`, deployment is skipped for that network. Checking and + /// deploying on a single fork reads each dependency exactly once, so a + /// transient RPC inconsistency on a redundant second read cannot report an + /// already-deployed dependency as missing and abort an otherwise-valid + /// deploy. Each network is handled independently: the Zoltu deploy is + /// idempotent (an existing contract is skipped), so a failure on one network + /// leaves the others intact and the script can simply be re-run, which is why + /// no separate all-network pre-flight is needed. /// @param vm The Vm instance to use for forking and broadcasting. /// @param networks The list of network names to deploy to. - /// @param forkIds The fork ids returned by `checkDependencies`, reused per - /// network so the deploy runs on the same validated snapshot. /// @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 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. function deployToNetworks( Vm vm, string[] memory networks, - uint256[] memory forkIds, address deployer, bytes memory creationCode, string memory contractPath, address expectedAddress, - bytes32 expectedCodeHash + bytes32 expectedCodeHash, + address[] memory dependencies ) internal returns (address deployedAddress) { if (networks.length == 0) { revert NoNetworks(); } for (uint256 i = 0; i < networks.length; i++) { + vm.createSelectFork(networks[i]); console2.log("Deploying to network:", networks[i]); - // Reuse the fork that checkDependencies already validated for this - // network, rather than creating a fresh fork at a newer head. The - // dependencies were verified on this exact snapshot; a fresh fork - // re-reads them on a different one, where a transient RPC - // inconsistency can report an already-deployed dependency as missing - // and abort an otherwise-valid deploy. - vm.selectFork(forkIds[i]); console2.log("Block number:", block.number); + console2.log(" - Zoltu Factory:", ZOLTU_FACTORY); + // Zoltu factory must exist with the expected codehash. + if (ZOLTU_FACTORY.code.length == 0) { + revert MissingDependency(networks[i], ZOLTU_FACTORY); + } + if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); + } + + // Each dependency must already be deployed on this network. + for (uint256 j = 0; j < dependencies.length; j++) { + console2.log(" - Dependency:", dependencies[j]); + if (dependencies[j].code.length == 0) { + revert MissingDependency(networks[i], dependencies[j]); + } + } + vm.startBroadcast(deployer); if (expectedAddress.code.length == 0) { console2.log(" - Deploying via Zoltu"); @@ -317,9 +296,8 @@ library LibRainDeploy { console2.log("Deploying from address:", deployer); - uint256[] memory forkIds = checkDependencies(vm, networks, dependencies); deployedAddress = deployToNetworks( - vm, networks, forkIds, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash + vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies ); } } diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index 09f1885..e4c88ce 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -211,101 +211,34 @@ contract LibRainDeployTest is Test { this.externalDeployAndBroadcast(networks, 1, hex"", "", address(0), bytes32(0), dependencies); } - /// `checkDependencies` MUST revert with `NoNetworks` when given an empty - /// networks array. - function testCheckDependenciesNoNetworksReverts() external { - string[] memory networks = new string[](0); - address[] memory dependencies = new address[](0); - vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); - this.externalCheckDependencies(networks, dependencies); - } - /// `deployToNetworks` MUST revert with `NoNetworks` when given an empty /// networks array. function testDeployToNetworksNoNetworksReverts() external { string[] memory networks = new string[](0); - uint256[] memory forkIds = new uint256[](0); + address[] memory dependencies = new address[](0); vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NoNetworks.selector)); - this.externalDeployToNetworks(networks, forkIds, address(this), hex"", "", address(0), bytes32(0)); - } - - /// `deployToNetworks` MUST reuse the fork that `checkDependencies` validated - /// for each network, never create a fresh one. A fresh fork re-reads the - /// dependencies on a newer snapshot, where a transient RPC inconsistency can - /// report an already-deployed dependency as missing and abort a valid - /// deploy. After `deployToNetworks` returns, the active fork MUST equal the - /// fork id `checkDependencies` returned for the last network, proving the - /// validated fork was reused rather than replaced (a fresh fork would leave - /// a different active fork id). - function testDeployToNetworksReusesValidatedFork() external { - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.BASE; - address[] memory dependencies = new address[](1); - dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; - - uint256[] memory forkIds = this.externalCheckDependencies(networks, dependencies); - assertEq(forkIds.length, networks.length); - - // Target the already-deployed Zoltu factory so the deploy is skipped; - // this test asserts fork reuse, not the deployment itself. - this.externalDeployToNetworks( - networks, - forkIds, - address(this), - hex"", - "", - LibRainDeploy.ZOLTU_FACTORY, - LibRainDeploy.ZOLTU_FACTORY_CODEHASH - ); - - assertEq(vm.activeFork(), forkIds[networks.length - 1]); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } - /// With multiple networks, `deployToNetworks` MUST select each network's own - /// validated fork by index (`forkIds[i]`), not a fixed one. Two distinct - /// networks produce two distinct fork ids; after deploying to both, the - /// active fork MUST be the SECOND network's fork, which only holds if the - /// per-index selection is correct (selecting `forkIds[0]` for every network - /// would leave the first network's fork active instead). - function testDeployToNetworksReusesPerNetworkFork() external { + /// `deployToNetworks` MUST deploy to every network in the list, forking each + /// independently. Two networks that start without the target both end up with + /// the deterministic contract, and the call returns its address. + function testDeployToNetworksMultipleNetworks() external { string[] memory networks = new string[](2); networks[0] = LibRainDeploy.BASE; networks[1] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](1); - dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; - - uint256[] memory forkIds = this.externalCheckDependencies(networks, dependencies); - assertEq(forkIds.length, networks.length); - // Each network gets its own fork. - assertTrue(forkIds[0] != forkIds[1]); + address[] memory dependencies = new address[](0); - // Target the already-deployed Zoltu factory so the deploy is skipped on - // both networks; this test asserts per-network fork selection. - this.externalDeployToNetworks( + address result = this.externalDeployToNetworks( networks, - forkIds, address(this), - hex"", - "", - LibRainDeploy.ZOLTU_FACTORY, - LibRainDeploy.ZOLTU_FACTORY_CODEHASH + type(MockDeployable).creationCode, + "test/src/lib/MockDeployable.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies ); - - // The loop ends on the last network, so the active fork must be the last - // network's fork. Selecting forkIds[0] for every iteration would leave - // forkIds[0] active here, killing that mutation. - assertEq(vm.activeFork(), forkIds[1]); - } - - /// External wrapper for `checkDependencies` so that `vm.expectRevert` - /// works at the correct call depth. - /// @param networks The list of network names to check. - /// @param dependencies The dependency addresses to check. - function externalCheckDependencies(string[] memory networks, address[] memory dependencies) - external - returns (uint256[] memory forkIds) - { - forkIds = LibRainDeploy.checkDependencies(vm, networks, dependencies); + assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } /// External wrapper for `deployToNetworks` so that `vm.expectRevert` @@ -316,18 +249,19 @@ contract LibRainDeployTest is Test { /// @param contractPath The contract path for verification commands. /// @param expectedAddress The expected deterministic address. /// @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. function externalDeployToNetworks( string[] memory networks, - uint256[] memory forkIds, address deployer, bytes memory creationCode, string memory contractPath, address expectedAddress, - bytes32 expectedCodeHash + bytes32 expectedCodeHash, + address[] memory dependencies ) external returns (address deployedAddress) { deployedAddress = LibRainDeploy.deployToNetworks( - vm, networks, forkIds, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash + vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies ); } @@ -366,11 +300,9 @@ contract LibRainDeployTest is Test { /// `deployToNetworks` MUST revert with `UnexpectedDeployedAddress` when the /// deployed address does not match the expected address. function testUnexpectedDeployedAddressReverts() external { - uint256 forkId = vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; - uint256[] memory forkIds = new uint256[](1); - forkIds[0] = forkId; + address[] memory dependencies = new address[](0); vm.expectRevert( abi.encodeWithSelector( LibRainDeploy.UnexpectedDeployedAddress.selector, @@ -379,18 +311,16 @@ contract LibRainDeployTest is Test { ) ); this.externalDeployToNetworks( - networks, forkIds, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0) + networks, address(this), type(MockDeployable).creationCode, "", address(0xdead), bytes32(0), dependencies ); } /// `deployToNetworks` MUST revert with `UnexpectedDeployedCodeHash` when the /// deployed code hash does not match the expected code hash. function testUnexpectedDeployedCodeHashReverts() external { - uint256 forkId = vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; - uint256[] memory forkIds = new uint256[](1); - forkIds[0] = forkId; + address[] memory dependencies = new address[](0); address expectedAddress = 0xC24016f209562fc151e5Ab7F88694ED5775feb36; bytes32 wrongCodeHash = bytes32(uint256(1)); vm.expectRevert( @@ -401,7 +331,7 @@ contract LibRainDeployTest is Test { ) ); this.externalDeployToNetworks( - networks, forkIds, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash + networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies ); } @@ -428,31 +358,31 @@ contract LibRainDeployTest is Test { function testDeployToNetworksSkipsWhenAlreadyDeployed() external { vm.makePersistent(address(this)); - uint256 forkId = vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + 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; - uint256[] memory forkIds = new uint256[](1); - forkIds[0] = forkId; + address[] memory dependencies = new address[](0); address result = this.externalDeployToNetworks( networks, - forkIds, address(this), type(MockDeployable).creationCode, "test/src/lib/MockDeployable.sol:MockDeployable", 0xC24016f209562fc151e5Ab7F88694ED5775feb36, - 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483 + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies ); assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } - /// `checkDependencies` MUST revert with `MissingDependency` when the - /// Zoltu factory has no code on the network. - function testCheckDependenciesMissingZoltuFactoryReverts() external { + /// `deployToNetworks` MUST revert with `MissingDependency` when the Zoltu + /// factory has no code on the network. + function testDeployToNetworksMissingZoltuFactoryReverts() external { + vm.makePersistent(address(this)); vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); @@ -466,12 +396,13 @@ contract LibRainDeployTest is Test { LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, LibRainDeploy.ZOLTU_FACTORY ) ); - this.externalCheckDependencies(networks, dependencies); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } - /// `checkDependencies` MUST revert with `DependencyChanged` when the - /// Zoltu factory exists but has a wrong codehash. - function testCheckDependenciesZoltuFactoryCodehashChangedReverts() external { + /// `deployToNetworks` MUST revert with `DependencyChanged` when the Zoltu + /// factory exists but has a wrong codehash. + function testDeployToNetworksZoltuFactoryCodehashChangedReverts() external { + vm.makePersistent(address(this)); vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); vm.makePersistent(LibRainDeploy.ZOLTU_FACTORY); vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex"00"); @@ -489,12 +420,12 @@ contract LibRainDeployTest is Test { keccak256(hex"00") ) ); - this.externalCheckDependencies(networks, dependencies); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } - /// `checkDependencies` MUST revert with `MissingDependency` when a - /// dependency has no code on the network. - function testMissingDependencyReverts() external { + /// `deployToNetworks` MUST revert with `MissingDependency` when a dependency + /// has no code on the network. + function testDeployToNetworksMissingDependencyReverts() external { string[] memory networks = new string[](1); networks[0] = LibRainDeploy.ARBITRUM_ONE; address[] memory dependencies = new address[](1); @@ -505,6 +436,6 @@ contract LibRainDeployTest is Test { LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) ) ); - this.externalCheckDependencies(networks, dependencies); + this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } } From 8d912977d3e569b70e20797ac07388e6556c8fee Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 10:55:55 +0000 Subject: [PATCH 4/5] fix(deploy): bind the unused createSelectFork return for slither The merged deployToNetworks discarded the fork id returned by vm.createSelectFork, which slither flags as an unused return. Bind and reference it, matching the idiom used elsewhere for this call. Co-Authored-By: Claude Opus 4.8 --- src/lib/LibRainDeploy.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 17f21de..5619017 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -219,7 +219,10 @@ library LibRainDeploy { revert NoNetworks(); } for (uint256 i = 0; i < networks.length; i++) { - vm.createSelectFork(networks[i]); + // createSelectFork returns a fork id that is not needed here; bind + // and reference it so the unused-return lint stays satisfied. + uint256 forkId = vm.createSelectFork(networks[i]); + (forkId); console2.log("Deploying to network:", networks[i]); console2.log("Block number:", block.number); From 9156d28bd50f316f37f3d7007a891428fb953013 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 29 Jun 2026 11:36:46 +0000 Subject: [PATCH 5/5] fix(deploy): skip already-deployed networks before dependency validation deployToNetworks validated the Zoltu factory and dependencies before the already-deployed skip path, so a network that no longer needs deploying could still revert MissingDependency / DependencyChanged on a rerun, re-introducing the transient-read failure this change exists to remove for the no-op case. Reorder so the expectedAddress-has-code skip is taken first; dependencies are only validated on the deploy path. The deployed contract codehash is still verified on both paths. Adds testDeployToNetworksSkipsAlreadyDeployedWithMissingDependency, which deploys the target then reruns with a missing dependency and asserts the network is skipped without reverting (fails on the old order). Co-Authored-By: Claude Opus 4.8 --- src/lib/LibRainDeploy.sol | 46 +++++++++++++++++--------------- test/src/lib/LibRainDeploy.t.sol | 31 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 5619017..962a2cb 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -226,40 +226,44 @@ library LibRainDeploy { console2.log("Deploying to network:", networks[i]); console2.log("Block number:", block.number); - console2.log(" - Zoltu Factory:", ZOLTU_FACTORY); - // Zoltu factory must exist with the expected codehash. - if (ZOLTU_FACTORY.code.length == 0) { - revert MissingDependency(networks[i], ZOLTU_FACTORY); - } - if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { - revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); - } - - // Each dependency must already be deployed on this network. - for (uint256 j = 0; j < dependencies.length; j++) { - console2.log(" - Dependency:", dependencies[j]); - if (dependencies[j].code.length == 0) { - revert MissingDependency(networks[i], dependencies[j]); + if (expectedAddress.code.length == 0) { + // Nothing is deployed here yet, so the Zoltu factory and every + // dependency must be present before broadcasting the deploy. + console2.log(" - Zoltu Factory:", ZOLTU_FACTORY); + if (ZOLTU_FACTORY.code.length == 0) { + revert MissingDependency(networks[i], ZOLTU_FACTORY); + } + if (ZOLTU_FACTORY.codehash != ZOLTU_FACTORY_CODEHASH) { + revert DependencyChanged(networks[i], ZOLTU_FACTORY, ZOLTU_FACTORY_CODEHASH, ZOLTU_FACTORY.codehash); + } + for (uint256 j = 0; j < dependencies.length; j++) { + console2.log(" - Dependency:", dependencies[j]); + if (dependencies[j].code.length == 0) { + revert MissingDependency(networks[i], dependencies[j]); + } } - } - vm.startBroadcast(deployer); - if (expectedAddress.code.length == 0) { console2.log(" - Deploying via Zoltu"); + vm.startBroadcast(deployer); deployedAddress = deployZoltu(creationCode); + vm.stopBroadcast(); + if (deployedAddress != expectedAddress) { + revert UnexpectedDeployedAddress(expectedAddress, deployedAddress); + } } else { + // Already deployed on this network. The Zoltu deploy is + // idempotent, so skip it without checking dependencies: an + // already-deployed network needs neither the Zoltu factory nor + // its dependencies present to remain deployed, which keeps a + // rerun a clean no-op here. console2.log(" - Code already exists at expected address, skipping deployment"); deployedAddress = expectedAddress; } console2.log(" - Final Address:", deployedAddress); - if (deployedAddress != expectedAddress) { - revert UnexpectedDeployedAddress(expectedAddress, deployedAddress); - } console2.log(" - Verifying code hash"); if (expectedCodeHash != deployedAddress.codehash) { revert UnexpectedDeployedCodeHash(expectedCodeHash, deployedAddress.codehash); } - vm.stopBroadcast(); console2.log("manual verification command:"); console2.log( diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index e4c88ce..c08ec54 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -379,6 +379,37 @@ contract LibRainDeployTest is Test { assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); } + /// `deployToNetworks` MUST skip an already-deployed network WITHOUT checking + /// its dependencies. A rerun on a network that no longer needs deployment is + /// a clean no-op even when a dependency is now missing, because the + /// dependency check only guards the deploy path. + function testDeployToNetworksSkipsAlreadyDeployedWithMissingDependency() 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; + // A dependency with no code: it would revert MissingDependency on the + // deploy path, but the target is already deployed so it is never checked. + address[] memory dependencies = new address[](1); + dependencies[0] = address(0xdead); + + address result = this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/src/lib/MockDeployable.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + } + /// `deployToNetworks` MUST revert with `MissingDependency` when the Zoltu /// factory has no code on the network. function testDeployToNetworksMissingZoltuFactoryReverts() external {