From e33edcffd1f181df7f679c63c261dd0c4240fc98 Mon Sep 17 00:00:00 2001 From: David Meister Date: Mon, 21 Sep 2026 11:25:19 +0000 Subject: [PATCH] test: assert the shared-tail clauses through cloneDeterministicOpenSalt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five clauses of the shared clone tail were asserted only through `cloneDeterministic`: that `initialize` is the only call the factory makes on the proxy — stated normatively on `cloneDeterministicOpenSalt` itself — that `NewClone` precedes it, `CloneDeploymentFailed` at a codeless address with a nonzero nonce, the implementation-code guard running before the `CREATE2`, and the literal `0` value that keeps a factory's ETH out of the clone. Both entry points funnel into one body, so a mutant of any of those lines is killed by whichever entry point happens to cover it and the per-entry-point gap is invisible to mutation. The suite's own convention is to assert shared-tail behaviour through both entry points, as `cloneAndInitialize.t.sol` does. Co-Authored-By: Claude Opus 5 (1M context) --- ...FactoryV4.cloneDeterministicOpenSalt.t.sol | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol index fe67bc1..edefb7a 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -5,7 +5,7 @@ pragma solidity =0.8.25; import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; -import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; import { ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN @@ -13,11 +13,13 @@ import { import { LibICloneableFactoryV4, CloneAddressOccupied, + CloneDeploymentFailed, InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableCallRecorder} from "test/concrete/TestCloneableCallRecorder.sol"; import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest @@ -444,4 +446,88 @@ contract LibICloneableFactoryV4CloneDeterministicOpenSaltTest is Test { predictedMax ); } + + /// `initialize` is the FIRST thing called on the fresh proxy, and the only + /// thing: "MUST NOT call anything else on the proxy first", which + /// `ICloneableFactoryV4` states on this function. `TestCloneable` only + /// exposes its end state, so the recorder is used instead — it appends the + /// selector of every call the proxy receives, including ones whose result + /// the factory would discard, and the whole recorded sequence is asserted + /// rather than just its first entry. + function testCloneDeterministicOpenSaltInitializeIsTheOnlyCall(bytes32 salt, bytes memory data) external { + TestCloneableCallRecorder implementation = new TestCloneableCallRecorder(); + + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + bytes4[] memory selectors = TestCloneableCallRecorder(child).selectors(); + assertEq(selectors.length, 1); + assertEq(selectors[0], ICloneableV2.initialize.selector); + assertEq(TestCloneableCallRecorder(child).sData(), data); + } + + /// `NewClone` is emitted BEFORE `initialize` runs, so an indexer replaying + /// the log stream sees the clone announced before anything the clone itself + /// says about being initialized. The recorder logs from inside `initialize`, + /// which is what makes the relative order observable at all; + /// `…OpenSaltEvent` asserts the count alone and cannot see it. + function testCloneDeterministicOpenSaltEventPrecedesInitialize(bytes32 salt, bytes memory data) external { + TestCloneableCallRecorder implementation = new TestCloneableCallRecorder(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 2); + + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)")))); + assertEq(entries[0].data, abi.encode(address(this), address(implementation), child, salt, data)); + + assertEq(entries[1].emitter, child); + assertEq(entries[1].topics[0], bytes32(uint256(keccak256("Initializing(bytes)")))); + assertEq(entries[1].data, abi.encode(data)); + } + + /// A codeless clone address with a nonzero nonce fails the `CREATE2` + /// itself: `CloneDeploymentFailed`. + function testCloneDeterministicOpenSaltNonceOnlyCollisionReverts(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + vm.setNonce(predicted, 1); + + vm.expectRevert(abi.encodeWithSelector(CloneDeploymentFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + } + + /// The implementation-code guard runs before the occupancy check: with the + /// salt taken and the implementation codeless, + /// `ZeroImplementationCodeSize` reverts, not `CloneAddressOccupied`. + function testCloneDeterministicOpenSaltCodeGuardRunsBeforeCreate2(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + assertTrue(child.code.length > 0); + + vm.etch(address(implementation), ""); + assertEq(address(implementation).code.length, 0); + + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + } + + /// `CREATE2` is given a literal `0` value, so a factory that is holding ETH + /// endows the clone with none of it. Balances on both sides are asserted, + /// so neither "the clone got funded" nor "the factory got drained" can + /// pass. + function testCloneDeterministicOpenSaltNoEthForwarded(bytes32 salt, bytes memory data, uint256 balance) external { + balance = bound(balance, 1, type(uint128).max); + TestCloneable implementation = new TestCloneable(); + vm.deal(address(I_CLONE_FACTORY), balance); + + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(child.balance, 0); + assertEq(address(I_CLONE_FACTORY).balance, balance); + } }