diff --git a/audit/mutation-test-scans.json b/audit/mutation-test-scans.json new file mode 100644 index 0000000..b0da986 --- /dev/null +++ b/audit/mutation-test-scans.json @@ -0,0 +1,22 @@ +[ + { + "timestamp": "2026-07-25T20:20:00Z", + "commit": "4422e291234e776de79d3bd8ffa4249a2a9d2abb", + "publishedTag": "sol-v0.1.4", + "commitsAheadOfTag": 2, + "scope": "whole repo", + "tool": "adversarial-mutation-test", + "skillVersion": "0.27.0", + "summary": { + "units": 1, + "behaviours": 91, + "killedByExistingTests": 67, + "gapsFilled": 18, + "equivalentMutants": 5, + "unkillableDefensiveGuards": 1, + "candidates": 3, + "confirmed": 3, + "filed": ["#16", "#17", "#18"] + } + } +] diff --git a/test/src/lib/LibRainDeploy.t.sol b/test/src/lib/LibRainDeploy.t.sol index c08ec54..6cb51a0 100644 --- a/test/src/lib/LibRainDeploy.t.sol +++ b/test/src/lib/LibRainDeploy.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibRainDeploy} from "../../../src/lib/LibRainDeploy.sol"; +import {MockAddressRevertingFactory} from "./MockAddressRevertingFactory.sol"; import {MockDeployable} from "./MockDeployable.sol"; import {MockReverter} from "./MockReverter.sol"; @@ -12,6 +13,26 @@ import {MockReverter} from "./MockReverter.sol"; /// that need `vm.expectRevert` at the correct call depth, and for functions /// that require a storage mapping reference. contract LibRainDeployTest is Test { + /// 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 Arbitrum One. + uint256 constant ARBITRUM_ONE_CHAIN_ID = 42161; + + /// 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; + /// External wrapper for `isStartBlock` so that it can be called /// externally in tests. /// @param target The contract address to check. @@ -150,10 +171,15 @@ contract LibRainDeployTest is Test { } /// `ZOLTU_FACTORY_CODEHASH` MUST match the actual codehash of the Zoltu - /// factory on a forked network. + /// 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 { - vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); - assertEq(LibRainDeploy.ZOLTU_FACTORY.codehash, LibRainDeploy.ZOLTU_FACTORY_CODEHASH); + 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 @@ -239,6 +265,11 @@ contract LibRainDeployTest is Test { dependencies ); assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + // The fork left selected is the last network in the list, and the + // contract exists there with the expected code, so the loop reached + // beyond the first network. + assertEq(block.chainid, ARBITRUM_ONE_CHAIN_ID); + assertEq(result.codehash, 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483); } /// External wrapper for `deployToNetworks` so that `vm.expectRevert` @@ -272,6 +303,14 @@ contract LibRainDeployTest is Test { 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 { @@ -469,4 +508,153 @@ contract LibRainDeployTest is Test { ); this.externalDeployToNetworks(networks, address(this), hex"", "", address(0), bytes32(0), dependencies); } + + /// `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/src/lib/MockDeployable.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + assertEq(result, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + } + + /// `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); + 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""); + vm.etch(address(0), hex"00"); + assertGt(address(0).code.length, 0); + + vm.expectRevert(abi.encodeWithSelector(LibRainDeploy.DeployFailed.selector, true, address(0))); + this.externalDeployZoltu(type(MockDeployable).creationCode); + } + + /// `deployZoltu` MUST NOT forward the caller's value to the factory. The + /// deployed mock has a non payable constructor, so forwarded value would + /// fail the deployment outright. + function testDeployZoltuDoesNotForwardValue() external { + vm.createSelectFork(LibRainDeploy.ARBITRUM_ONE); + vm.deal(address(this), 1 ether); + address deployed = this.externalDeployZoltuPayable{value: 1}(type(MockDeployable).creationCode); + assertEq(deployed, 0xC24016f209562fc151e5Ab7F88694ED5775feb36); + assertEq(deployed.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); + uint64 nonceBefore = vm.getNonce(deployer); + + this.externalDeployAndBroadcast( + networks, + deployerPrivateKey, + type(MockDeployable).creationCode, + "test/src/lib/MockDeployable.sol:MockDeployable", + 0xC24016f209562fc151e5Ab7F88694ED5775feb36, + 0xc1a263a0b50505687a5140c7964ec5c947329e7d03410306fee68cc3620c5483, + dependencies + ); + + assertEq(vm.getNonce(deployer), nonceBefore + 1); + } } diff --git a/test/src/lib/MockAddressRevertingFactory.sol b/test/src/lib/MockAddressRevertingFactory.sol new file mode 100644 index 0000000..4b38fea --- /dev/null +++ b/test/src/lib/MockAddressRevertingFactory.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +/// @title MockAddressRevertingFactory +/// Zoltu factory stand-in whose calls always fail, reverting with exactly the +/// twenty bytes of its own address. A caller that reads its call output buffer +/// without checking the call succeeded sees the address of a contract that has +/// code. +contract MockAddressRevertingFactory { + fallback() external { + bytes20 self = bytes20(address(this)); + assembly ("memory-safe") { + mstore(0, self) + revert(0, 20) + } + } +}