diff --git a/src/lib/LibRainDeploy.sol b/src/lib/LibRainDeploy.sol index 186f67a..9103d92 100644 --- a/src/lib/LibRainDeploy.sol +++ b/src/lib/LibRainDeploy.sol @@ -32,6 +32,14 @@ library LibRainDeploy { /// Thrown when no networks are provided for deployment. error NoNetworks(); + /// Thrown when attempting to find the deploy block of a contract that has + /// no code at the current block. + error NotDeployed(address target); + + /// Thrown when the target already has code at the start block, meaning + /// the deploy may have happened before the search range. + error DeployedBeforeStartBlock(address target, uint256 startBlock); + /// Zoltu factory is the same on every network. address constant ZOLTU_FACTORY = 0x7A0D94F55792C434d74a40883C6ed8545E406D12; @@ -56,6 +64,82 @@ library LibRainDeploy { /// Config name for Polygon network. string constant POLYGON = "polygon"; + /// Checks whether a block is the first block where a contract with the + /// expected code hash exists. True when the target has the expected code + /// hash at `blockNumber` and does NOT have it at `blockNumber - 1`. At + /// block 0, only the first condition is checked. The fork is restored to + /// its original block number after checking. + /// @param vm The Vm instance for fork manipulation. + /// @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 isStartBlock(Vm vm, address target, bytes32 expectedCodeHash, uint256 blockNumber) + internal + returns (bool isStart) + { + uint256 originalBlock = block.number; + vm.rollFork(blockNumber); + isStart = target.codehash == expectedCodeHash; + if (isStart && blockNumber > 0) { + vm.rollFork(blockNumber - 1); + isStart = target.codehash != expectedCodeHash; + } + vm.rollFork(originalBlock); + } + + /// Finds the block number at which a contract was first deployed by binary + /// searching the fork history. Requires an active fork with archive access + /// back to `startBlock`. The fork is restored to its original block + /// number before returning. The target's code hash is verified against the + /// expected value before searching. The result is validated via + /// `isStartBlock`. + /// @param vm The Vm instance for fork manipulation. + /// @param target The contract address to search for. + /// @param expectedCodeHash The expected code hash of the target contract. + /// @param startBlock The earliest block to search from. The target MUST + /// NOT have the expected code hash at this block. + /// @return deployBlock The first block number where `target` has the + /// expected code hash. + function findDeployBlock(Vm vm, address target, bytes32 expectedCodeHash, uint256 startBlock) + internal + returns (uint256 deployBlock) + { + if (target.code.length == 0) { + revert NotDeployed(target); + } + if (target.codehash != expectedCodeHash) { + revert UnexpectedDeployedCodeHash(expectedCodeHash, target.codehash); + } + + uint256 originalBlock = block.number; + + // Verify the target does not already have the expected code at + // startBlock. If it does, the deploy happened before our search + // range and the result would be meaningless. + vm.rollFork(startBlock); + if (target.codehash == expectedCodeHash) { + vm.rollFork(originalBlock); + revert DeployedBeforeStartBlock(target, startBlock); + } + + uint256 low = startBlock; + uint256 high = originalBlock; + + while (low < high) { + uint256 mid = (low + high) / 2; + vm.rollFork(mid); + if (target.codehash == expectedCodeHash) { + high = mid; + } else { + low = mid + 1; + } + } + + deployBlock = low; + vm.rollFork(originalBlock); + } + /// Etches the Zoltu factory bytecode into the factory address. Useful for /// networks where the factory is not yet deployed. /// @param vm The Vm instance to use for etching. diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index a7bea67..f14c2f7 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -28,6 +28,131 @@ contract MockReverter { 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. + /// @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. + function testFindDeployBlockNotDeployedReverts() external { + vm.createSelectFork(LibRainDeploy.BASE); + 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. + function testFindDeployBlockWrongCodeHashReverts() external { + vm.createSelectFork(LibRainDeploy.BASE); + bytes32 wrongHash = bytes32(uint256(1)); + vm.expectRevert( + abi.encodeWithSelector( + LibRainDeploy.UnexpectedDeployedCodeHash.selector, wrongHash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH + ) + ); + 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 {