From ab1fa4cc34afb4d819624ed681af0f7d57e4c850 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 16:13:36 +0000 Subject: [PATCH 1/3] test(fork): split LibRainDeploy tests by whether they need a chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 31 of the repo's 37 `createSelectFork` calls sat in `LibRainDeployTest`, which is not a `Chain` contract, so the `forge test --no-match-contract Chain` gate `CLAUDE.md` documents as fork-free forked 31 times and an RPC outage failed it. Audited all 31. Nine bought nothing: eight tests etch every address they read, so the fork was replaced by `etchZoltuFactory` or by asserting the precondition the bare EVM already holds, and `testDeployZoltuRevertsWhenFactoryCallFailsWithAddressData` is deleted as fully subsumed by `testDeployZoltuFailedCallReportsZeroAddress` — same etch, same call, and the surviving test asserts the exact payload where the deleted one asserted only the selector. The remaining 29 tests genuinely read a chain and move to `LibRainDeployChainTest`: the `isStartBlock`/`findDeployBlock` history search, the Zoltu factory bytecode and codehash pins, and everything reaching `deployToNetworks`' or `checkResolvedAddressesOnNetworks\' per-network fork loop. Co-Authored-By: Claude Opus 5 (1M context) --- test/abstract/LibRainDeployTestBase.sol | 207 ++++++ test/src/lib/LibRainDeploy.t.sol | 842 +----------------------- test/src/lib/LibRainDeployChain.t.sol | 629 ++++++++++++++++++ 3 files changed, 864 insertions(+), 814 deletions(-) create mode 100644 test/abstract/LibRainDeployTestBase.sol create mode 100644 test/src/lib/LibRainDeployChain.t.sol diff --git a/test/abstract/LibRainDeployTestBase.sol b/test/abstract/LibRainDeployTestBase.sol new file mode 100644 index 0000000..c45b658 --- /dev/null +++ b/test/abstract/LibRainDeployTestBase.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibRainDeploy} from "../../src/lib/LibRainDeploy.sol"; +import {IAddressRegistryV1} from "../../src/interface/IAddressRegistryV1.sol"; +import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../src/concrete/AddressRegistry.sol"; +import {MockResolvedOwner} from "../concrete/MockResolvedOwner.sol"; +import {MockDeployable} from "../concrete/MockDeployable.sol"; +import {MockDeployableV2} from "../concrete/MockDeployableV2.sol"; + +/// @title LibRainDeployTestBase +/// The fixtures and external wrappers `LibRainDeploy`'s tests share, so that +/// splitting those tests by whether they need a chain does not duplicate any of +/// it. External wrappers are used for library functions that need +/// `vm.expectRevert` at the correct call depth. +/// +/// The split itself is `LibRainDeployTest` (reads no chain state, forks nothing) +/// and `LibRainDeployChainTest` (forks, directly or through the library). It is +/// by contract because `--no-match-contract Chain` is what selects it. +abstract contract LibRainDeployTestBase 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. + /// @param expectedCodeHash The code hash to look for. + /// @param blockNumber The block number to check. + /// @return isStart True if the contract first appears at this block. + function externalIsStartBlock(address target, bytes32 expectedCodeHash, uint256 blockNumber) + external + returns (bool isStart) + { + isStart = LibRainDeploy.isStartBlock(vm, target, expectedCodeHash, blockNumber); + } + + /// External wrapper for `findDeployBlock` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param target The contract address to search for. + /// @param expectedCodeHash The expected code hash of the target. + /// @param startBlock The earliest block to search from. + /// @return deployBlock The first block number where `target` has code. + function externalFindDeployBlock(address target, bytes32 expectedCodeHash, uint256 startBlock) + external + returns (uint256 deployBlock) + { + deployBlock = LibRainDeploy.findDeployBlock(vm, target, expectedCodeHash, startBlock); + } + + /// External wrapper for `deployAndBroadcast` so that + /// `vm.expectRevert` works at the correct call depth. + /// @param networks The list of network names to deploy to. + /// @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 expectedCodeHash The expected code hash of the deployed contract. + /// @param dependencies The dependency addresses to check. + /// @return deployedAddress The deployed contract address. + function externalDeployAndBroadcast( + string[] memory networks, + uint256 deployerPrivateKey, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies + ) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployAndBroadcast( + vm, + networks, + deployerPrivateKey, + creationCode, + contractPath, + expectedAddress, + expectedCodeHash, + dependencies + ); + } + + /// External wrapper for `deployToNetworks` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param networks The list of network names to deploy to. + /// @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 externalDeployToNetworks( + string[] memory networks, + address deployer, + bytes memory creationCode, + string memory contractPath, + address expectedAddress, + bytes32 expectedCodeHash, + address[] memory dependencies + ) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployToNetworks( + vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies + ); + } + + /// External wrapper for `deployZoltu` so that it can be called on a fork. + /// @param creationCode The creation code to deploy via the Zoltu factory. + /// @return deployedAddress The address of the deployed contract. + function externalDeployZoltu(bytes memory creationCode) external returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployZoltu(creationCode); + } + + /// External wrapper for `deployZoltu` that carries value, so that what the + /// library does with the caller's value is observable. + /// @param creationCode The creation code to deploy via the Zoltu factory. + /// @return deployedAddress The address of the deployed contract. + function externalDeployZoltuPayable(bytes memory creationCode) external payable returns (address deployedAddress) { + deployedAddress = LibRainDeploy.deployZoltu(creationCode); + } + + /// Deploys `AddressRegistry` through the Zoltu factory (which lands it at + /// its pinned address), binds `name` to `account` as root, then deploys a + /// consumer that resolves `name` once in its constructor. + /// @param name The name to bind and resolve. + /// @param account The address to bind it to. + /// @return registry The deployed registry. + /// @return consumer The deployed consumer holding the resolved address. + function deployRegistryAndConsumer(bytes32 name, address account) + internal + returns (IAddressRegistryV1 registry, MockResolvedOwner consumer) + { + LibRainDeploy.etchZoltuFactory(vm); + registry = IAddressRegistryV1(LibRainDeploy.deployZoltu(type(AddressRegistry).creationCode)); + vm.prank(ADDRESS_REGISTRY_ROOT); + registry.register(name, account); + consumer = new MockResolvedOwner(name); + } + + /// The calldata for reading `MockResolvedOwner`'s stored address. + /// @return The single-element read call list. + function ownerReadCalls() internal pure returns (bytes[] memory) { + bytes[] memory readCalls = new bytes[](1); + readCalls[0] = abi.encodeWithSignature("iOwner()"); + return readCalls; + } + + /// A single-element expected address list. + /// @param account The expected address. + /// @return The list. + function expected(address account) internal pure returns (address[] memory) { + address[] memory expectedAddresses = new address[](1); + expectedAddresses[0] = account; + return expectedAddresses; + } + + /// External wrapper for `checkResolvedAddresses` so that `vm.expectRevert` + /// works at the correct call depth. + /// @param network The network name, for the error only. + /// @param target The deployed contract to read. + /// @param readCalls The calldata for each read. + /// @param expectedAddresses The address each read MUST answer with. + function externalCheckResolvedAddresses( + string memory network, + address target, + bytes[] memory readCalls, + address[] memory expectedAddresses + ) external view { + LibRainDeploy.checkResolvedAddresses(network, target, readCalls, expectedAddresses); + } + + /// External wrapper for `checkResolvedAddressesOnNetworks` so that + /// `vm.expectRevert` works at the correct call depth. + /// @param networks The list of network names to check. + /// @param target The deployed contract to read on each network. + /// @param readCalls The calldata for each read. + /// @param expectedAddresses The address each read MUST answer with. + function externalCheckResolvedAddressesOnNetworks( + string[] memory networks, + address target, + bytes[] memory readCalls, + address[] memory expectedAddresses + ) external { + LibRainDeploy.checkResolvedAddressesOnNetworks(vm, networks, target, readCalls, expectedAddresses); + } +} diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index b1f8bdb..e01f310 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -2,150 +2,49 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibRainDeployTestBase} from "../../abstract/LibRainDeployTestBase.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; import {IAddressRegistryV1} from "../../../src/interface/IAddressRegistryV1.sol"; -import {AddressRegistry, ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol"; +import {ADDRESS_REGISTRY_ROOT} from "../../../src/concrete/AddressRegistry.sol"; import {MockAddressRevertingFactory} from "../../concrete/MockAddressRevertingFactory.sol"; import {MockResolvedOwner} from "../../concrete/MockResolvedOwner.sol"; import {MockDirtyWordOwner} from "../../concrete/MockDirtyWordOwner.sol"; -import {MockAddressRevertingFactory} from "../../concrete/MockAddressRevertingFactory.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 -/// that need `vm.expectRevert` at the correct call depth, and for functions -/// that require a storage mapping reference. -contract LibRainDeployTest is Test { - /// Base allocates the OP Stack WETH9 predeploy in its genesis block, so - /// this address has code at block 0. - address constant BASE_GENESIS_PREDEPLOY = 0x4200000000000000000000000000000000000006; - - /// Code hash of the Base genesis WETH9 predeploy, fixed by the genesis - /// allocation and therefore identical at block 0 and block 1. - bytes32 constant BASE_GENESIS_PREDEPLOY_CODEHASH = - 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c; - - /// The block at which the Zoltu factory first has its code on Base. - uint256 constant ZOLTU_BASE_DEPLOY_BLOCK = 1117029; - - /// Chain id of Base. - uint256 constant BASE_CHAIN_ID = 8453; - - /// Chain id of Arbitrum One. - uint256 constant ARBITRUM_ONE_CHAIN_ID = 42161; - +/// The `LibRainDeploy` tests that read no chain state, and therefore fork +/// nothing. Every one of them either touches no address the chain would supply, +/// or etches the whole of what it touches — so a fork would only slow it down +/// and tie it to an RPC endpoint being up. +/// +/// The name deliberately has no `Chain` in it: this contract is what +/// `forge test --no-match-contract Chain` runs, and it MUST stay runnable with +/// no `.env` at all. A test added here that needs a chain belongs in +/// `LibRainDeployChainTest` instead. +contract LibRainDeployTest is LibRainDeployTestBase { /// The address the Zoltu factory derives for empty creation code, i.e. /// CREATE2 over the factory address, a zero salt and the hash of empty /// creation code. An account is created there but it has no code. address constant ZOLTU_EMPTY_CREATION_CODE_ADDRESS = 0x5DC93B79FBDD6f26Ed9540597C78eD5893F9aC7A; - /// 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. - /// @param expectedCodeHash The code hash to look for. - /// @param blockNumber The block number to check. - /// @return isStart True if the contract first appears at this block. - function externalIsStartBlock(address target, bytes32 expectedCodeHash, uint256 blockNumber) - external - returns (bool isStart) - { - isStart = LibRainDeploy.isStartBlock(vm, target, expectedCodeHash, blockNumber); - } - - /// External wrapper for `findDeployBlock` so that `vm.expectRevert` - /// works at the correct call depth. - /// @param target The contract address to search for. - /// @param expectedCodeHash The expected code hash of the target. - /// @param startBlock The earliest block to search from. - /// @return deployBlock The first block number where `target` has code. - function externalFindDeployBlock(address target, bytes32 expectedCodeHash, uint256 startBlock) - external - returns (uint256 deployBlock) - { - deployBlock = LibRainDeploy.findDeployBlock(vm, target, expectedCodeHash, startBlock); - } - - /// `isStartBlock` MUST return false when the target has no code at the - /// given block. - function testIsStartBlockNoCode() external { - vm.createSelectFork(LibRainDeploy.BASE); - assertFalse(LibRainDeploy.isStartBlock(vm, address(0xdead), bytes32(uint256(1)), block.number)); - } - - /// `isStartBlock` MUST return false when the target has the expected - /// code hash at both the given block and the block before it. - function testIsStartBlockCodeAtBothBlocks() external { - vm.createSelectFork(LibRainDeploy.BASE); - // The Zoltu factory exists at the current block and the block - // before it, so this is not a start block. - assertFalse( - LibRainDeploy.isStartBlock( - vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, block.number - ) - ); - } - - /// `isStartBlock` MUST return true when the target has the expected code - /// hash at the given block but not at the block before it. Uses the - /// actual Zoltu factory deploy block found by `findDeployBlock`. - function testIsStartBlockAtDeployBlock() external { - vm.createSelectFork(LibRainDeploy.BASE); - uint256 deployBlock = - LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); - assertTrue( - LibRainDeploy.isStartBlock( - vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock - ) - ); - } - - /// `isStartBlock` MUST restore the fork to its original block number. - function testIsStartBlockRestoresFork() external { - vm.createSelectFork(LibRainDeploy.BASE); - uint256 originalBlock = block.number; - LibRainDeploy.isStartBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); - assertEq(block.number, originalBlock); - } - /// `findDeployBlock` MUST revert with `NotDeployed` when the target - /// address has no code on the current fork. + /// address has no code. The guard runs before the search does, so no fork + /// history is read to reach it. function testFindDeployBlockNotDeployedReverts() external { - vm.createSelectFork(LibRainDeploy.BASE); + assertEq(address(0xdead).code.length, 0); vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.NotDeployed.selector, address(0xdead))); this.externalFindDeployBlock(address(0xdead), bytes32(0), 0); } /// `findDeployBlock` MUST revert with `UnexpectedDeployedCodeHash` when - /// the target's code hash does not match the expected value. + /// the target's code hash does not match the expected value. Also runs + /// before the search, so the factory is etched rather than forked to: what + /// the etched code hash is on a real network is + /// `LibRainDeployChainTest.testZoltuFactoryCodehash`'s subject, not this + /// one's. function testFindDeployBlockWrongCodeHashReverts() external { - vm.createSelectFork(LibRainDeploy.BASE); + LibRainDeploy.etchZoltuFactory(vm); bytes32 wrongHash = bytes32(uint256(1)); vm.expectRevert( abi.encodeWithSelector( @@ -155,42 +54,6 @@ contract LibRainDeployTest is Test { this.externalFindDeployBlock(LibRainDeploy.ZOLTU_FACTORY, wrongHash, 0); } - /// `findDeployBlock` MUST revert with `DeployedBeforeStartBlock` when - /// the target already has code at the start block. - function testFindDeployBlockDeployedBeforeStartBlockReverts() external { - vm.createSelectFork(LibRainDeploy.BASE); - // Use the current block as startBlock — the Zoltu factory already - // exists here, so the function should revert. - uint256 startBlock = block.number; - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.DeployedBeforeStartBlock.selector, LibRainDeploy.ZOLTU_FACTORY, startBlock - ) - ); - this.externalFindDeployBlock(LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, startBlock); - } - - /// `findDeployBlock` MUST return a block that `isStartBlock` confirms, - /// and the fork MUST be restored to the original block number. - /// Uses Base because the public Base RPC has full archive access. - function testFindDeployBlockZoltuFactory() external { - vm.createSelectFork(LibRainDeploy.BASE); - uint256 originalBlock = block.number; - - uint256 deployBlock = - LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); - - // Fork must be restored to the original block. - assertEq(block.number, originalBlock); - - // The result must be a valid start block. - assertTrue( - LibRainDeploy.isStartBlock( - vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock - ) - ); - } - /// `supportedNetworks` MUST return exactly 5 networks in the expected /// order matching the library constants. function testSupportedNetworks() external pure { @@ -203,25 +66,6 @@ contract LibRainDeployTest is Test { assertEq(networks[4], LibRainDeploy.POLYGON); } - /// `ZOLTU_FACTORY_CODEHASH` MUST match the actual codehash of the Zoltu - /// factory on every supported network. Every name in `supportedNetworks` - /// MUST also be a configured fork alias, otherwise it cannot be deployed - /// to at all. - function testZoltuFactoryCodehash() external { - string[] memory networks = LibRainDeploy.supportedNetworks(); - for (uint256 i = 0; i < networks.length; i++) { - vm.createSelectFork(networks[i]); - assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, networks[i]); - } - } - - /// `ZOLTU_FACTORY_BYTECODE` MUST match the actual runtime bytecode of the - /// Zoltu factory on a forked network. - function testZoltuFactoryBytecode() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - assertEq(LibRainDeploy.ZOLTU_FACTORY.code, LibRainDeploy.ZOLTU_FACTORY_BYTECODE); - } - /// `etchZoltuFactory` MUST place the correct bytecode and codehash at the /// Zoltu factory address. function testEtchZoltuFactory() external { @@ -231,37 +75,6 @@ contract LibRainDeployTest is Test { assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH); } - /// External wrapper for `deployAndBroadcast` so that - /// `vm.expectRevert` works at the correct call depth. - /// @param networks The list of network names to deploy to. - /// @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 expectedCodeHash The expected code hash of the deployed contract. - /// @param dependencies The dependency addresses to check. - /// @return deployedAddress The deployed contract address. - function externalDeployAndBroadcast( - string[] memory networks, - uint256 deployerPrivateKey, - bytes memory creationCode, - string memory contractPath, - address expectedAddress, - bytes32 expectedCodeHash, - address[] memory dependencies - ) external returns (address deployedAddress) { - deployedAddress = LibRainDeploy.deployAndBroadcast( - vm, - networks, - deployerPrivateKey, - creationCode, - contractPath, - expectedAddress, - expectedCodeHash, - dependencies - ); - } - /// Empty networks array MUST revert with `NoNetworks`. function testNoNetworksReverts() external { string[] memory networks = new string[](0); @@ -279,107 +92,12 @@ contract LibRainDeployTest is Test { this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } - /// `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[](0); - - address result = this.externalDeployToNetworks( - networks, - address(this), - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - assertEq(result, mockDeployableAddress()); - - // The returned address is the same for every network, so it says - // nothing about how many networks were visited. What does is the state - // each fork was left in. - // - // `deployToNetworks` creates one fork per network, in list order, and - // this test creates none of its own, so those forks are ids 0 and 1. - // Selecting each in turn and asserting BOTH the chain it is on and - // that the contract is deployed there is what pins "every network in - // the list": a loop that stops early never creates the second fork, a - // loop that starts late puts the wrong chain at id 0, and a loop that - // forks `networks[0]` every iteration puts the same chain at both ids. - vm.selectFork(0); - assertEq(block.chainid, BASE_CHAIN_ID); - assertEq(result.codehash, mockDeployableCodeHash()); - - vm.selectFork(1); - assertEq(block.chainid, ARBITRUM_ONE_CHAIN_ID); - assertEq(result.codehash, mockDeployableCodeHash()); - } - - /// External wrapper for `deployToNetworks` so that `vm.expectRevert` - /// works at the correct call depth. - /// @param networks The list of network names to deploy to. - /// @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 externalDeployToNetworks( - string[] memory networks, - address deployer, - bytes memory creationCode, - string memory contractPath, - address expectedAddress, - bytes32 expectedCodeHash, - address[] memory dependencies - ) external returns (address deployedAddress) { - deployedAddress = LibRainDeploy.deployToNetworks( - vm, networks, deployer, creationCode, contractPath, expectedAddress, expectedCodeHash, dependencies - ); - } - - /// External wrapper for `deployZoltu` so that it can be called on a fork. - /// @param creationCode The creation code to deploy via the Zoltu factory. - /// @return deployedAddress The address of the deployed contract. - function externalDeployZoltu(bytes memory creationCode) external returns (address deployedAddress) { - deployedAddress = LibRainDeploy.deployZoltu(creationCode); - } - - /// External wrapper for `deployZoltu` that carries value, so that what the - /// library does with the caller's value is observable. - /// @param creationCode The creation code to deploy via the Zoltu factory. - /// @return deployedAddress The address of the deployed contract. - function externalDeployZoltuPayable(bytes memory creationCode) external payable returns (address deployedAddress) { - deployedAddress = LibRainDeploy.deployZoltu(creationCode); - } - - /// `deployZoltu` MUST deploy a contract via the Zoltu factory and return - /// the deterministic address predicted by the factory's nonce. - 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. - // It is the address the factory returns for the creation code this - // repo's compiler settings emit for `MockDeployable` — solc 0.8.25, - // optimizer on at 100,000 runs, targeting cancun. Those settings are - // now pinned exactly in `foundry.toml`, because this repo's deploy pins - // depend on them; that is what makes a literal here stable at all, and - // moving any of them moves this address. - assertEq(deployed, 0x0c04367b381F8Ca252aD2516F1Eac2b9B2ca928F); - } - /// `deployZoltu` MUST revert with `DeployFailed` when the Zoltu factory /// has no code. function testDeployZoltuRevertsNoFactory() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); + // The precondition, stated rather than arranged: nothing has etched the + // factory, so a call to it succeeds and returns nothing. + assertEq(LibRainDeploy.ZOLTU_FACTORY.code.length, 0); vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, true, address(0))); this.externalDeployZoltu(type(MockDeployable).creationCode); } @@ -387,7 +105,7 @@ contract LibRainDeployTest is Test { /// `deployZoltu` MUST revert with `DeployFailed` when the creation code /// has a reverting constructor (success=false from factory call). function testDeployZoltuRevertsRevertingConstructor() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + LibRainDeploy.etchZoltuFactory(vm); vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, false, address(0))); this.externalDeployZoltu(type(MockReverter).creationCode); } @@ -397,7 +115,6 @@ contract LibRainDeployTest is Test { /// call output buffer holds revert data on the failure path, so anything /// read from it there is not an address the factory returned. function testDeployZoltuFailedCallReportsZeroAddress() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); vm.etch(LibRainDeploy.ZOLTU_FACTORY, address(new MockAddressRevertingFactory()).code); // The discriminating power of this test is entirely the shape of the @@ -432,239 +149,6 @@ contract LibRainDeployTest is Test { ); } - /// `deployToNetworks` MUST revert with `UnexpectedDeployedCodeHash` when the - /// deployed code hash does not match the expected code hash. - function testUnexpectedDeployedCodeHashReverts() external { - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - address expectedAddress = mockDeployableAddress(); - bytes32 wrongCodeHash = bytes32(uint256(1)); - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongCodeHash, mockDeployableCodeHash() - ) - ); - this.externalDeployToNetworks( - networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies - ); - } - - /// `deployAndBroadcast` MUST check dependencies, deploy - /// via Zoltu, and return the correct address with the correct codehash. - function testDeployAndBroadcastHappyPath() external { - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - address deployed = this.externalDeployAndBroadcast( - networks, - 1, - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - assertEq(deployed, mockDeployableAddress()); - } - - /// `deployToNetworks` MUST skip deployment and return the expected address - /// when code already exists there, provided the codehash matches. - function testDeployToNetworksSkipsWhenAlreadyDeployed() external { - vm.makePersistent(address(this)); - - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); - assertEq(deployed, mockDeployableAddress()); - vm.makePersistent(deployed); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - - address result = this.externalDeployToNetworks( - networks, - address(this), - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - assertEq(result, mockDeployableAddress()); - } - - /// `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, mockDeployableAddress()); - 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/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - assertEq(result, mockDeployableAddress()); - } - - /// `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""); - - 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), - type(MockDeployable).creationCode, - "", - mockDeployableAddress(), - bytes32(0), - dependencies - ); - } - - /// `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"); - - 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), - type(MockDeployable).creationCode, - "", - mockDeployableAddress(), - bytes32(0), - dependencies - ); - } - - /// `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); - dependencies[0] = address(0xdead); - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) - ) - ); - this.externalDeployToNetworks( - networks, - address(this), - type(MockDeployable).creationCode, - "", - mockDeployableAddress(), - 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, mockDeployableAddress()); - 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, mockDeployableAddress(), mockDeployableV2Address() - ) - ); - // 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/concrete/MockDeployableV2.sol:MockDeployableV2", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - - // The new contract was not deployed anywhere. - assertEq(mockDeployableV2Address().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. @@ -684,104 +168,6 @@ contract LibRainDeployTest is Test { ); } - /// `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, mockDeployableAddress(), LibRainDeploy.ZOLTU_FACTORY - ) - ); - this.externalDeployToNetworks( - networks, - address(this), - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - } - - /// Deploys `AddressRegistry` through the Zoltu factory (which lands it at - /// its pinned address), binds `name` to `account` as root, then deploys a - /// consumer that resolves `name` once in its constructor. - /// @param name The name to bind and resolve. - /// @param account The address to bind it to. - /// @return registry The deployed registry. - /// @return consumer The deployed consumer holding the resolved address. - function deployRegistryAndConsumer(bytes32 name, address account) - internal - returns (IAddressRegistryV1 registry, MockResolvedOwner consumer) - { - LibRainDeploy.etchZoltuFactory(vm); - registry = IAddressRegistryV1(LibRainDeploy.deployZoltu(type(AddressRegistry).creationCode)); - vm.prank(ADDRESS_REGISTRY_ROOT); - registry.register(name, account); - consumer = new MockResolvedOwner(name); - } - - /// The calldata for reading `MockResolvedOwner`'s stored address. - /// @return The single-element read call list. - function ownerReadCalls() internal pure returns (bytes[] memory) { - bytes[] memory readCalls = new bytes[](1); - readCalls[0] = abi.encodeWithSignature("iOwner()"); - return readCalls; - } - - /// A single-element expected address list. - /// @param account The expected address. - /// @return The list. - function expected(address account) internal pure returns (address[] memory) { - address[] memory expectedAddresses = new address[](1); - expectedAddresses[0] = account; - return expectedAddresses; - } - - /// External wrapper for `checkResolvedAddresses` so that `vm.expectRevert` - /// works at the correct call depth. - /// @param network The network name, for the error only. - /// @param target The deployed contract to read. - /// @param readCalls The calldata for each read. - /// @param expectedAddresses The address each read MUST answer with. - function externalCheckResolvedAddresses( - string memory network, - address target, - bytes[] memory readCalls, - address[] memory expectedAddresses - ) external view { - LibRainDeploy.checkResolvedAddresses(network, target, readCalls, expectedAddresses); - } - - /// External wrapper for `checkResolvedAddressesOnNetworks` so that - /// `vm.expectRevert` works at the correct call depth. - /// @param networks The list of network names to check. - /// @param target The deployed contract to read on each network. - /// @param readCalls The calldata for each read. - /// @param expectedAddresses The address each read MUST answer with. - function externalCheckResolvedAddressesOnNetworks( - string[] memory networks, - address target, - bytes[] memory readCalls, - address[] memory expectedAddresses - ) external { - LibRainDeploy.checkResolvedAddressesOnNetworks(vm, networks, target, readCalls, expectedAddresses); - } - /// `checkResolvedAddresses` MUST pass when the deployed contract holds the /// address the deployment expects. function testCheckResolvedAddressesMatch(bytes32 name, address account) external { @@ -989,163 +375,23 @@ contract LibRainDeployTest is Test { this.externalCheckResolvedAddressesOnNetworks(networks, address(this), readCalls, expectedAddresses); } - /// `checkResolvedAddressesOnNetworks` MUST fork each network in turn and - /// pass when the deployed contract holds the expected address on all of - /// them. The deployment is made persistent so the same contract is present - /// on every fork, which is the state a real multi-network deploy leaves - /// behind. - /// - /// Two networks rather than `supportedNetworks()`. What is under test is - /// that the loop visits every network it is given, which two prove as well - /// as five; the roster itself is `testSupportedNetworks`'s job. These are - /// the two networks the rest of this suite forks, so the test does not - /// depend on the reliability of RPC endpoints nothing else here touches. - function testCheckResolvedAddressesOnNetworksEachNetwork() external { - bytes32 name = keccak256("testCheckResolvedAddressesOnNetworksEachNetwork"); - address account = address(0xf00); - (, MockResolvedOwner consumer) = deployRegistryAndConsumer(name, account); - vm.makePersistent(address(consumer)); - - string[] memory networks = new string[](2); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - networks[1] = LibRainDeploy.BASE; - - LibRainDeploy.checkResolvedAddressesOnNetworks( - vm, networks, address(consumer), ownerReadCalls(), expected(account) - ); - } - - /// `checkResolvedAddressesOnNetworks` MUST fail on the network that - /// disagrees, and MUST name it. - function testCheckResolvedAddressesOnNetworksMismatchReverts() external { - bytes32 name = keccak256("testCheckResolvedAddressesOnNetworksMismatchReverts"); - address account = address(0xf00); - address wrong = address(0xba4); - (, MockResolvedOwner consumer) = deployRegistryAndConsumer(name, account); - vm.makePersistent(address(consumer)); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.BASE; - - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.UnexpectedResolvedAddress.selector, - LibRainDeploy.BASE, - address(consumer), - uint256(0), - wrong, - account - ) - ); - this.externalCheckResolvedAddressesOnNetworks(networks, address(consumer), ownerReadCalls(), expected(wrong)); - } - - /// `deployToNetworks` MUST deploy when every dependency has code on the - /// network, i.e. a present dependency is not treated as missing. - function testDeployToNetworksPresentDependencyDeploys() external { - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](1); - dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; - - address result = this.externalDeployToNetworks( - networks, - address(this), - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - assertEq(result, mockDeployableAddress()); - assertEq(result.codehash, mockDeployableCodeHash()); - } - - /// `isStartBlock` MUST return true at block 0 for a target that already - /// has the expected code hash in the genesis allocation. There is no block - /// before genesis, so the code hash at the given block alone decides. - function testIsStartBlockGenesisAllocationAtBlockZero() external { - vm.createSelectFork(LibRainDeploy.BASE); - assertTrue(LibRainDeploy.isStartBlock(vm, BASE_GENESIS_PREDEPLOY, BASE_GENESIS_PREDEPLOY_CODEHASH, 0)); - } - - /// `isStartBlock` MUST return false at block 1 for a target from the - /// genesis allocation, because block 0 already has the same code hash. - function testIsStartBlockGenesisAllocationAtBlockOne() external { - vm.createSelectFork(LibRainDeploy.BASE); - assertFalse(LibRainDeploy.isStartBlock(vm, BASE_GENESIS_PREDEPLOY, BASE_GENESIS_PREDEPLOY_CODEHASH, 1)); - } - - /// `isStartBlock` MUST return false for the block immediately after the - /// deploy block. The block compared against is the immediately preceding - /// one, which already has the expected code hash. - function testIsStartBlockOneBlockAfterDeployBlock() external { - vm.createSelectFork(LibRainDeploy.BASE); - assertFalse( - LibRainDeploy.isStartBlock( - vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, ZOLTU_BASE_DEPLOY_BLOCK + 1 - ) - ); - } - - /// `findDeployBlock` MUST return the exact block at which the target first - /// has the expected code hash, including when that is the block the fork is - /// currently at. - function testFindDeployBlockExactZoltuBaseDeployBlock() external { - vm.createSelectFork(LibRainDeploy.BASE, ZOLTU_BASE_DEPLOY_BLOCK); - assertEq( - LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0), - ZOLTU_BASE_DEPLOY_BLOCK - ); - } - - /// `findDeployBlock` MUST leave the fork on its original block when it - /// reverts because the target already has the expected code hash at the - /// start block. - function testFindDeployBlockRestoresForkOnDeployedBeforeStartBlockRevert() external { - vm.createSelectFork(LibRainDeploy.BASE); - uint256 originalBlock = block.number; - vm.expectRevert( - abi.encodeWithSelector( - LibRainDeploy.DeployedBeforeStartBlock.selector, LibRainDeploy.ZOLTU_FACTORY, ZOLTU_BASE_DEPLOY_BLOCK - ) - ); - this.externalFindDeployBlock( - LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, ZOLTU_BASE_DEPLOY_BLOCK - ); - assertEq(block.number, originalBlock); - } - /// `deployZoltu` MUST revert when the factory call succeeds but leaves no /// code at the resulting address. Empty creation code creates an account /// with no runtime code, which is not a deployment. function testDeployZoltuRevertsEmptyCreationCode() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + LibRainDeploy.etchZoltuFactory(vm); vm.expectRevert( abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, true, ZOLTU_EMPTY_CREATION_CODE_ADDRESS) ); this.externalDeployZoltu(hex""); } - /// `deployZoltu` MUST NOT report a deployment when the factory call fails, - /// even when the failed call leaves the address of a contract that does - /// have code in the call output buffer. - function testDeployZoltuRevertsWhenFactoryCallFailsWithAddressData() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - vm.etch(LibRainDeploy.ZOLTU_FACTORY, address(new MockAddressRevertingFactory()).code); - assertGt(LibRainDeploy.ZOLTU_FACTORY.code.length, 0); - - vm.expectPartialRevert(LibRainDeploy.DeployFailed.selector); - this.externalDeployZoltu(type(MockDeployable).creationCode); - } - /// `deployZoltu` MUST NOT report the zero address as a deployment, even /// when the zero address has code. function testDeployZoltuRevertsZeroAddressWithCode() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); // A call to an address with no code succeeds and returns nothing, so // the factory yields the zero address. - vm.etch(LibRainDeploy.ZOLTU_FACTORY, hex""); + assertEq(LibRainDeploy.ZOLTU_FACTORY.code.length, 0); vm.etch(address(0), hex"00"); assertGt(address(0).code.length, 0); @@ -1157,43 +403,11 @@ contract LibRainDeployTest is Test { /// deployed mock has a non payable constructor, so forwarded value would /// fail the deployment outright. function testDeployZoltuDoesNotForwardValue() external { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + LibRainDeploy.etchZoltuFactory(vm); vm.deal(address(this), 1 ether); address deployed = this.externalDeployZoltuPayable{value: 1}(type(MockDeployable).creationCode); assertEq(deployed, mockDeployableAddress()); assertEq(deployed.balance, 0); assertEq(LibRainDeploy.ZOLTU_FACTORY.balance, 0); } - - /// `deployAndBroadcast` MUST broadcast as the address derived from the - /// given private key. - function testDeployAndBroadcastUsesDeployerFromPrivateKey() external { - uint256 deployerPrivateKey = 0xA11CE; - address deployer = vm.addr(deployerPrivateKey); - - string[] memory networks = new string[](1); - networks[0] = LibRainDeploy.ARBITRUM_ONE; - address[] memory dependencies = new address[](0); - - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - // Stated rather than assumed: the key is a test constant that has never - // transacted on any supported network, so its nonce is zero on a fresh - // fork. That is what makes the nonce read after the call — which lands - // on the fork `deployToNetworks` made, not this one — comparable to - // this baseline at all. - uint64 nonceBefore = vm.getNonce(deployer); - assertEq(nonceBefore, 0); - - this.externalDeployAndBroadcast( - networks, - deployerPrivateKey, - type(MockDeployable).creationCode, - "test/concrete/MockDeployable.sol:MockDeployable", - mockDeployableAddress(), - mockDeployableCodeHash(), - dependencies - ); - - assertEq(vm.getNonce(deployer), nonceBefore + 1); - } } diff --git a/test/src/lib/LibRainDeployChain.t.sol b/test/src/lib/LibRainDeployChain.t.sol new file mode 100644 index 0000000..ec43476 --- /dev/null +++ b/test/src/lib/LibRainDeployChain.t.sol @@ -0,0 +1,629 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {LibRainDeployTestBase} from "../../abstract/LibRainDeployTestBase.sol"; +import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; +import {MockResolvedOwner} from "../../concrete/MockResolvedOwner.sol"; +import {MockDeployable} from "../../concrete/MockDeployable.sol"; +import {MockDeployableV2} from "../../concrete/MockDeployableV2.sol"; + +/// @title LibRainDeployChainTest +/// The `LibRainDeploy` tests that genuinely read a chain, and so cannot run +/// without a reachable RPC endpoint for every network they name. Three kinds, +/// and nothing else belongs here: +/// +/// 1. The fork-history search. `isStartBlock` and `findDeployBlock` roll a fork +/// backwards and read the target's code hash at each block, so their subject +/// IS a chain's history — there is nothing to etch. +/// 2. The Zoltu factory pins. `ZOLTU_FACTORY_CODEHASH` and +/// `ZOLTU_FACTORY_BYTECODE` are constants asserted against the factory as +/// actually deployed, and `testDeployZoltu` / `testZoltuAddressMatchesFactoryDeploy` +/// take the LIVE factory as the oracle for the derivation. Etching any of +/// them would leave the constants checking themselves. +/// 3. Everything that reaches `deployToNetworks`' or +/// `checkResolvedAddressesOnNetworks`' per-network loop, both of which call +/// `vm.createSelectFork` themselves. The loop is the thing under test, so it +/// cannot be avoided from here. +/// +/// `Chain` in the name is load bearing: `forge test --no-match-contract Chain` +/// is the offline gate, and this contract is what it excludes. Its sibling +/// `LibRainDeployTest` is the rest, and forks nothing. +contract LibRainDeployChainTest is LibRainDeployTestBase { + /// Base allocates the OP Stack WETH9 predeploy in its genesis block, so + /// this address has code at block 0. + address constant BASE_GENESIS_PREDEPLOY = 0x4200000000000000000000000000000000000006; + + /// Code hash of the Base genesis WETH9 predeploy, fixed by the genesis + /// allocation and therefore identical at block 0 and block 1. + bytes32 constant BASE_GENESIS_PREDEPLOY_CODEHASH = + 0x8a3a1f6a9f9dce633117adee5b458245835a8645a8c8726a26382a4622508b1c; + + /// The block at which the Zoltu factory first has its code on Base. + uint256 constant ZOLTU_BASE_DEPLOY_BLOCK = 1117029; + + /// Chain id of Base. + uint256 constant BASE_CHAIN_ID = 8453; + + /// Chain id of Arbitrum One. + uint256 constant ARBITRUM_ONE_CHAIN_ID = 42161; + + /// `isStartBlock` MUST return false when the target has no code at the + /// given block. + function testIsStartBlockNoCode() external { + vm.createSelectFork(LibRainDeploy.BASE); + assertFalse(LibRainDeploy.isStartBlock(vm, address(0xdead), bytes32(uint256(1)), block.number)); + } + + /// `isStartBlock` MUST return false when the target has the expected + /// code hash at both the given block and the block before it. + function testIsStartBlockCodeAtBothBlocks() external { + vm.createSelectFork(LibRainDeploy.BASE); + // The Zoltu factory exists at the current block and the block + // before it, so this is not a start block. + assertFalse( + LibRainDeploy.isStartBlock( + vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, block.number + ) + ); + } + + /// `isStartBlock` MUST return true when the target has the expected code + /// hash at the given block but not at the block before it. Uses the + /// actual Zoltu factory deploy block found by `findDeployBlock`. + function testIsStartBlockAtDeployBlock() external { + vm.createSelectFork(LibRainDeploy.BASE); + uint256 deployBlock = + LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); + assertTrue( + LibRainDeploy.isStartBlock( + vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock + ) + ); + } + + /// `isStartBlock` MUST restore the fork to its original block number. + function testIsStartBlockRestoresFork() external { + vm.createSelectFork(LibRainDeploy.BASE); + uint256 originalBlock = block.number; + LibRainDeploy.isStartBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); + assertEq(block.number, originalBlock); + } + + /// `findDeployBlock` MUST revert with `DeployedBeforeStartBlock` when + /// the target already has code at the start block. + function testFindDeployBlockDeployedBeforeStartBlockReverts() external { + vm.createSelectFork(LibRainDeploy.BASE); + // Use the current block as startBlock — the Zoltu factory already + // exists here, so the function should revert. + uint256 startBlock = block.number; + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DeployedBeforeStartBlock.selector, LibRainDeploy.ZOLTU_FACTORY, startBlock + ) + ); + this.externalFindDeployBlock(LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, startBlock); + } + + /// `findDeployBlock` MUST return a block that `isStartBlock` confirms, + /// and the fork MUST be restored to the original block number. + /// Uses Base because the public Base RPC has full archive access. + function testFindDeployBlockZoltuFactory() external { + vm.createSelectFork(LibRainDeploy.BASE); + uint256 originalBlock = block.number; + + uint256 deployBlock = + LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0); + + // Fork must be restored to the original block. + assertEq(block.number, originalBlock); + + // The result must be a valid start block. + assertTrue( + LibRainDeploy.isStartBlock( + vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, deployBlock + ) + ); + } + + /// `ZOLTU_FACTORY_CODEHASH` MUST match the actual codehash of the Zoltu + /// factory on every supported network. Every name in `supportedNetworks` + /// MUST also be a configured fork alias, otherwise it cannot be deployed + /// to at all. + function testZoltuFactoryCodehash() external { + string[] memory networks = LibRainDeploy.supportedNetworks(); + for (uint256 i = 0; i < networks.length; i++) { + vm.createSelectFork(networks[i]); + assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, networks[i]); + } + } + + /// `ZOLTU_FACTORY_BYTECODE` MUST match the actual runtime bytecode of the + /// Zoltu factory on a forked network. + function testZoltuFactoryBytecode() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + assertEq(LibRainDeploy.ZOLTU_FACTORY.code, LibRainDeploy.ZOLTU_FACTORY_BYTECODE); + } + + /// `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[](0); + + address result = this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + assertEq(result, mockDeployableAddress()); + + // The returned address is the same for every network, so it says + // nothing about how many networks were visited. What does is the state + // each fork was left in. + // + // `deployToNetworks` creates one fork per network, in list order, and + // this test creates none of its own, so those forks are ids 0 and 1. + // Selecting each in turn and asserting BOTH the chain it is on and + // that the contract is deployed there is what pins "every network in + // the list": a loop that stops early never creates the second fork, a + // loop that starts late puts the wrong chain at id 0, and a loop that + // forks `networks[0]` every iteration puts the same chain at both ids. + vm.selectFork(0); + assertEq(block.chainid, BASE_CHAIN_ID); + assertEq(result.codehash, mockDeployableCodeHash()); + + vm.selectFork(1); + assertEq(block.chainid, ARBITRUM_ONE_CHAIN_ID); + assertEq(result.codehash, mockDeployableCodeHash()); + } + + /// `deployZoltu` MUST deploy a contract via the Zoltu factory and return + /// the deterministic address predicted by the factory's nonce. + 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. + // It is the address the factory returns for the creation code this + // repo's compiler settings emit for `MockDeployable` — solc 0.8.25, + // optimizer on at 100,000 runs, targeting cancun. Those settings are + // now pinned exactly in `foundry.toml`, because this repo's deploy pins + // depend on them; that is what makes a literal here stable at all, and + // moving any of them moves this address. + assertEq(deployed, 0x0c04367b381F8Ca252aD2516F1Eac2b9B2ca928F); + } + + /// `deployToNetworks` MUST revert with `UnexpectedDeployedCodeHash` when the + /// deployed code hash does not match the expected code hash. + function testUnexpectedDeployedCodeHashReverts() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + address expectedAddress = mockDeployableAddress(); + bytes32 wrongCodeHash = bytes32(uint256(1)); + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongCodeHash, mockDeployableCodeHash() + ) + ); + this.externalDeployToNetworks( + networks, address(this), type(MockDeployable).creationCode, "", expectedAddress, wrongCodeHash, dependencies + ); + } + + /// `deployAndBroadcast` MUST check dependencies, deploy + /// via Zoltu, and return the correct address with the correct codehash. + function testDeployAndBroadcastHappyPath() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + address deployed = this.externalDeployAndBroadcast( + networks, + 1, + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + assertEq(deployed, mockDeployableAddress()); + } + + /// `deployToNetworks` MUST skip deployment and return the expected address + /// when code already exists there, provided the codehash matches. + function testDeployToNetworksSkipsWhenAlreadyDeployed() external { + vm.makePersistent(address(this)); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); + assertEq(deployed, mockDeployableAddress()); + vm.makePersistent(deployed); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + address result = this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + assertEq(result, mockDeployableAddress()); + } + + /// `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, mockDeployableAddress()); + 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/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + assertEq(result, mockDeployableAddress()); + } + + /// `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""); + + 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), + type(MockDeployable).creationCode, + "", + mockDeployableAddress(), + bytes32(0), + dependencies + ); + } + + /// `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"); + + 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), + type(MockDeployable).creationCode, + "", + mockDeployableAddress(), + bytes32(0), + dependencies + ); + } + + /// `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); + dependencies[0] = address(0xdead); + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.MissingDependency.selector, LibRainDeploy.ARBITRUM_ONE, address(0xdead) + ) + ); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "", + mockDeployableAddress(), + 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, mockDeployableAddress()); + 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, mockDeployableAddress(), mockDeployableV2Address() + ) + ); + // 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/concrete/MockDeployableV2.sol:MockDeployableV2", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + + // The new contract was not deployed anywhere. + assertEq(mockDeployableV2Address().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, mockDeployableAddress(), LibRainDeploy.ZOLTU_FACTORY + ) + ); + this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + } + + /// `checkResolvedAddressesOnNetworks` MUST fork each network in turn and + /// pass when the deployed contract holds the expected address on all of + /// them. The deployment is made persistent so the same contract is present + /// on every fork, which is the state a real multi-network deploy leaves + /// behind. + /// + /// Two networks rather than `supportedNetworks()`. What is under test is + /// that the loop visits every network it is given, which two prove as well + /// as five; the roster itself is `testSupportedNetworks`'s job. These are + /// the two networks the rest of this suite forks, so the test does not + /// depend on the reliability of RPC endpoints nothing else here touches. + function testCheckResolvedAddressesOnNetworksEachNetwork() external { + bytes32 name = keccak256("testCheckResolvedAddressesOnNetworksEachNetwork"); + address account = address(0xf00); + (, MockResolvedOwner consumer) = deployRegistryAndConsumer(name, account); + vm.makePersistent(address(consumer)); + + string[] memory networks = new string[](2); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + networks[1] = LibRainDeploy.BASE; + + LibRainDeploy.checkResolvedAddressesOnNetworks( + vm, networks, address(consumer), ownerReadCalls(), expected(account) + ); + } + + /// `checkResolvedAddressesOnNetworks` MUST fail on the network that + /// disagrees, and MUST name it. + function testCheckResolvedAddressesOnNetworksMismatchReverts() external { + bytes32 name = keccak256("testCheckResolvedAddressesOnNetworksMismatchReverts"); + address account = address(0xf00); + address wrong = address(0xba4); + (, MockResolvedOwner consumer) = deployRegistryAndConsumer(name, account); + vm.makePersistent(address(consumer)); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.BASE; + + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedResolvedAddress.selector, + LibRainDeploy.BASE, + address(consumer), + uint256(0), + wrong, + account + ) + ); + this.externalCheckResolvedAddressesOnNetworks(networks, address(consumer), ownerReadCalls(), expected(wrong)); + } + + /// `deployToNetworks` MUST deploy when every dependency has code on the + /// network, i.e. a present dependency is not treated as missing. + function testDeployToNetworksPresentDependencyDeploys() external { + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](1); + dependencies[0] = LibRainDeploy.ZOLTU_FACTORY; + + address result = this.externalDeployToNetworks( + networks, + address(this), + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + assertEq(result, mockDeployableAddress()); + assertEq(result.codehash, mockDeployableCodeHash()); + } + + /// `isStartBlock` MUST return true at block 0 for a target that already + /// has the expected code hash in the genesis allocation. There is no block + /// before genesis, so the code hash at the given block alone decides. + function testIsStartBlockGenesisAllocationAtBlockZero() external { + vm.createSelectFork(LibRainDeploy.BASE); + assertTrue(LibRainDeploy.isStartBlock(vm, BASE_GENESIS_PREDEPLOY, BASE_GENESIS_PREDEPLOY_CODEHASH, 0)); + } + + /// `isStartBlock` MUST return false at block 1 for a target from the + /// genesis allocation, because block 0 already has the same code hash. + function testIsStartBlockGenesisAllocationAtBlockOne() external { + vm.createSelectFork(LibRainDeploy.BASE); + assertFalse(LibRainDeploy.isStartBlock(vm, BASE_GENESIS_PREDEPLOY, BASE_GENESIS_PREDEPLOY_CODEHASH, 1)); + } + + /// `isStartBlock` MUST return false for the block immediately after the + /// deploy block. The block compared against is the immediately preceding + /// one, which already has the expected code hash. + function testIsStartBlockOneBlockAfterDeployBlock() external { + vm.createSelectFork(LibRainDeploy.BASE); + assertFalse( + LibRainDeploy.isStartBlock( + vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, ZOLTU_BASE_DEPLOY_BLOCK + 1 + ) + ); + } + + /// `findDeployBlock` MUST return the exact block at which the target first + /// has the expected code hash, including when that is the block the fork is + /// currently at. + function testFindDeployBlockExactZoltuBaseDeployBlock() external { + vm.createSelectFork(LibRainDeploy.BASE, ZOLTU_BASE_DEPLOY_BLOCK); + assertEq( + LibRainDeploy.findDeployBlock(vm, LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, 0), + ZOLTU_BASE_DEPLOY_BLOCK + ); + } + + /// `findDeployBlock` MUST leave the fork on its original block when it + /// reverts because the target already has the expected code hash at the + /// start block. + function testFindDeployBlockRestoresForkOnDeployedBeforeStartBlockRevert() external { + vm.createSelectFork(LibRainDeploy.BASE); + uint256 originalBlock = block.number; + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.DeployedBeforeStartBlock.selector, LibRainDeploy.ZOLTU_FACTORY, ZOLTU_BASE_DEPLOY_BLOCK + ) + ); + this.externalFindDeployBlock( + LibRainDeploy.ZOLTU_FACTORY, LibRainDeploy.ZOLTU_FACTORY_CODEHASH, ZOLTU_BASE_DEPLOY_BLOCK + ); + assertEq(block.number, originalBlock); + } + + /// `deployAndBroadcast` MUST broadcast as the address derived from the + /// given private key. + function testDeployAndBroadcastUsesDeployerFromPrivateKey() external { + uint256 deployerPrivateKey = 0xA11CE; + address deployer = vm.addr(deployerPrivateKey); + + string[] memory networks = new string[](1); + networks[0] = LibRainDeploy.ARBITRUM_ONE; + address[] memory dependencies = new address[](0); + + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + // Stated rather than assumed: the key is a test constant that has never + // transacted on any supported network, so its nonce is zero on a fresh + // fork. That is what makes the nonce read after the call — which lands + // on the fork `deployToNetworks` made, not this one — comparable to + // this baseline at all. + uint64 nonceBefore = vm.getNonce(deployer); + assertEq(nonceBefore, 0); + + this.externalDeployAndBroadcast( + networks, + deployerPrivateKey, + type(MockDeployable).creationCode, + "test/concrete/MockDeployable.sol:MockDeployable", + mockDeployableAddress(), + mockDeployableCodeHash(), + dependencies + ); + + assertEq(vm.getNonce(deployer), nonceBefore + 1); + } +} From 84a8c49528beea92d33a25e2b54381f390cc795e Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 14 Aug 2026 16:23:40 +0000 Subject: [PATCH 2/3] docs: state the offline gate as the property the split now holds `CLAUDE.md` said `forge test --no-match-contract Chain` was "the whole snapshot gate", scoped to the verification abstracts, because that was as far as the split reached. It now reaches the whole repo: every `vm.createSelectFork` sits in a contract with `Chain` in its name, so the gate needs no `.env` at all. Stated as that property rather than as a list of forking contracts, with the three kinds of test that genuinely need a chain, so the question when adding a test is answerable without reading the suites. `testDeployZoltu`'s doc comment carries #33's wording rather than the "predicted by the factory's nonce" it moved with, so landing after #33 does not revert that fix. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 43 ++++++++++++++++++++++----- test/src/lib/LibRainDeployChain.t.sol | 3 +- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b67a857..bd98716 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,14 +61,42 @@ FLARE_RPC_URL=https://flare-api.flare.network/ext/C/rpc POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com ``` -All five are needed: `RainDeployVerifyChain` forks every network in -`supportedNetworks()`, so a missing or rate-limited endpoint fails it. Those -failures are `vm.createSelectFork` errors, distinct from the -`NotDeployedOnNetwork` a reachable network raises, and the snapshot contracts -run regardless: `forge test --no-match-contract Chain`. +All five are needed by the contracts that fork: `RainDeployVerifyChain` forks +every network in `supportedNetworks()`, so a missing or rate-limited endpoint +fails it. Those failures are `vm.createSelectFork` errors, distinct from the +`NotDeployedOnNetwork` a reachable network raises. These are referenced in `foundry.toml` under `[rpc_endpoints]`. +### `forge test --no-match-contract Chain` is the offline gate + +It needs no `.env` at all. Every `vm.createSelectFork` in the repo sits in a +contract with `Chain` in its name, so the gate forks nothing and no amount of +RPC weather can make it red. That is a property of where the forks are, not a +list to keep in step: the only way to break it is to fork from a contract the +gate runs. + +Which is the question to answer when adding a test. A test needs a chain only if +it reads state no fixture can supply, and three kinds do: + +1. The fork-history search. `isStartBlock` and `findDeployBlock` roll a fork + backwards and read the target at each block, so a chain's history IS their + subject and there is nothing to etch. +2. The Zoltu factory pins. `ZOLTU_FACTORY_BYTECODE` and `ZOLTU_FACTORY_CODEHASH` + are constants asserted against the factory as actually deployed, and + `zoltuAddress`' derivation is checked against what the live factory returns. + Etching any of it leaves the constants checking themselves. +3. Anything reaching `deployToNetworks`' or `checkResolvedAddressesOnNetworks`' + own per-network fork loop, which is the thing under test. + +Everything else etches what it reads and forks nothing — a test that etches the +factory and deploys into a bare EVM gets the same answer a fork gives it, only +without the endpoint. So `LibRainDeploy`'s suite is split by that question: +`LibRainDeployTest` (`test/src/lib/LibRainDeploy.t.sol`) forks nothing, +`LibRainDeployChainTest` (`test/src/lib/LibRainDeployChain.t.sol`) is the three +kinds above, and `LibRainDeployTestBase` (`test/abstract/`) holds the fixtures +and external wrappers both sides share so the split duplicates none of them. + ## Architecture **`src/lib/LibRainDeploy.sol`** — the deploy library: @@ -263,8 +291,9 @@ can set. Group 3 is what makes group 4's scope complete — a release group 4 is never handed is a release it cannot fail on. Group 4 lives in its own contract so an unreachable RPC endpoint fails only it, -never the snapshot assertions — `forge test --no-match-contract Chain` is the -whole snapshot gate, and nothing reachable from those contracts forks anything. +never the snapshot assertions. That is the offline gate's `Chain`-in-the-name +rule applied to the verification abstracts, and groups 1-3 are on the offline +side of it. A single recorded code hash per version can only be true if the runtime code is the same on every network, so a constructor reading `block.chainid` or similar diff --git a/test/src/lib/LibRainDeployChain.t.sol b/test/src/lib/LibRainDeployChain.t.sol index ec43476..09cc5dc 100644 --- a/test/src/lib/LibRainDeployChain.t.sol +++ b/test/src/lib/LibRainDeployChain.t.sol @@ -186,7 +186,8 @@ contract LibRainDeployChainTest is LibRainDeployTestBase { } /// `deployZoltu` MUST deploy a contract via the Zoltu factory and return - /// the deterministic address predicted by the factory's nonce. + /// the deterministic address the factory derives with `CREATE2` over the + /// creation code under a zero salt. function testDeployZoltu() external { vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); address deployed = this.externalDeployZoltu(type(MockDeployable).creationCode); From 4e4b77068cc5621bff67089019a5d351e9ba18c6 Mon Sep 17 00:00:00 2001 From: David Meister Date: Fri, 14 Aug 2026 17:23:55 +0000 Subject: [PATCH 3/3] docs: say what the Chain split buys, which is not working without a network Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 22 +++++++++++++--------- test/src/lib/LibRainDeployChain.t.sol | 6 +++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bd98716..d6c727b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,13 +68,17 @@ fails it. Those failures are `vm.createSelectFork` errors, distinct from the These are referenced in `foundry.toml` under `[rpc_endpoints]`. -### `forge test --no-match-contract Chain` is the offline gate +### `forge test --no-match-contract Chain` says whose fault a failure is -It needs no `.env` at all. Every `vm.createSelectFork` in the repo sits in a -contract with `Chain` in its name, so the gate forks nothing and no amount of -RPC weather can make it red. That is a property of where the forks are, not a -list to keep in step: the only way to break it is to fork from a contract the -gate runs. +Every `vm.createSelectFork` in the repo sits in a contract with `Chain` in its +name, so this selection forks nothing and no amount of RPC weather can make it +red. A red here is the code; a red only in the `Chain` contracts is the code or +the endpoints, and which one has to be read off the failure. That is what the +split buys — not the ability to work without a network, which is worth nothing +here, but a result that means something on its own. + +It is a property of where the forks are, not a list to keep in step: the only +way to break it is to fork from a contract this selection runs. Which is the question to answer when adding a test. A test needs a chain only if it reads state no fixture can supply, and three kinds do: @@ -291,9 +295,9 @@ can set. Group 3 is what makes group 4's scope complete — a release group 4 is never handed is a release it cannot fail on. Group 4 lives in its own contract so an unreachable RPC endpoint fails only it, -never the snapshot assertions. That is the offline gate's `Chain`-in-the-name -rule applied to the verification abstracts, and groups 1-3 are on the offline -side of it. +never the snapshot assertions. That is the `Chain`-in-the-name rule applied to +the verification abstracts, and groups 1-3 sit on the side of it that forks +nothing. A single recorded code hash per version can only be true if the runtime code is the same on every network, so a constructor reading `block.chainid` or similar diff --git a/test/src/lib/LibRainDeployChain.t.sol b/test/src/lib/LibRainDeployChain.t.sol index 09cc5dc..4ea1d03 100644 --- a/test/src/lib/LibRainDeployChain.t.sol +++ b/test/src/lib/LibRainDeployChain.t.sol @@ -26,9 +26,9 @@ import {MockDeployableV2} from "../../concrete/MockDeployableV2.sol"; /// `vm.createSelectFork` themselves. The loop is the thing under test, so it /// cannot be avoided from here. /// -/// `Chain` in the name is load bearing: `forge test --no-match-contract Chain` -/// is the offline gate, and this contract is what it excludes. Its sibling -/// `LibRainDeployTest` is the rest, and forks nothing. +/// `Chain` in the name is what selects it: `forge test --no-match-contract +/// Chain` excludes this contract, so a red there is never an endpoint. Its +/// sibling `LibRainDeployTest` is the rest, and forks nothing. contract LibRainDeployChainTest is LibRainDeployTestBase { /// Base allocates the OP Stack WETH9 predeploy in its genesis block, so /// this address has code at block 0.