From b48c4184fb14240088ca7b29f9af637910981558 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 07:48:28 +0000 Subject: [PATCH 1/7] AMT g4: pin the published interface surface and close the fixture-shape gaps Probe pass 1 against the pre-existing suite found the whole declaration-only half of this repo unguarded: the three deprecated interfaces, ICloneableFactoryV2 and ICloneableV2.InitializeSignatureFn are imported by nothing, and even ICloneableFactoryV3.NewClone survives having two of its three address parameters transposed, because that is invisible to every topic and every log data byte while telling every indexer the deployer is the clone. - test/src/lib/LibPublishedAbi.sol reads the compiled artifact, the only oracle for parameter names, same-typed parameter order, indexed flags and return types. - Declaration pins for ICloneableFactoryV3, ICloneableV2, ICloneableFactoryV2 and the three deprecated interfaces. - TestCloneableConformant, TestCloneableEmitter, TestCloneableReverter and TestCloneableV1Shaped: fixtures whose shape makes the once-only MUST, the typed-overload MUST, NewClone log ordering and initialize revert bubbling observable at all. - TestCloneFactory.t.sol pins the pure-delegation claim the concrete exists for. Co-Authored-By: Claude Opus 5 (1M context) --- foundry.toml | 8 + test/src/concrete/TestCloneFactory.t.sol | 126 +++++++++++++++ test/src/concrete/TestCloneableConformant.sol | 50 ++++++ test/src/concrete/TestCloneableEmitter.sol | 36 +++++ test/src/concrete/TestCloneableReverter.sol | 24 +++ test/src/concrete/TestCloneableV1Shaped.sol | 21 +++ .../interface/ICloneableFactoryV2.sol.t.sol | 71 ++++++++ .../interface/ICloneableFactoryV3.sol.t.sol | 139 ++++++++++++++++ test/src/interface/ICloneableV2.sol.t.sol | 151 ++++++++++++++++++ .../deprecated/DeprecatedInterfaces.t.sol | 118 ++++++++++++++ test/src/lib/LibPublishedAbi.sol | 38 +++++ 11 files changed, 782 insertions(+) create mode 100644 test/src/concrete/TestCloneFactory.t.sol create mode 100644 test/src/concrete/TestCloneableConformant.sol create mode 100644 test/src/concrete/TestCloneableEmitter.sol create mode 100644 test/src/concrete/TestCloneableReverter.sol create mode 100644 test/src/concrete/TestCloneableV1Shaped.sol create mode 100644 test/src/interface/ICloneableFactoryV2.sol.t.sol create mode 100644 test/src/interface/ICloneableFactoryV3.sol.t.sol create mode 100644 test/src/interface/ICloneableV2.sol.t.sol create mode 100644 test/src/interface/deprecated/DeprecatedInterfaces.t.sol create mode 100644 test/src/lib/LibPublishedAbi.sol diff --git a/foundry.toml b/foundry.toml index ce13e37..d32f561 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,14 @@ cbor_metadata = false libs = ["dependencies"] +# The interface half of this repo is a PUBLISHED ABI: downstream indexers and +# soldeer consumers decode `NewClone` and call `initialize` by NAME, from the +# compiled ABI. Parameter names, parameter order among same-typed parameters, +# `indexed` flags and return types are all invisible to the EVM but visible to +# those consumers, so the only place they can be pinned by a test is the +# compiled artifact. Read-only access to `out` exists for exactly that. +fs_permissions = [{ access = "read", path = "./out" }] + [fuzz] runs = 2048 diff --git a/test/src/concrete/TestCloneFactory.t.sol b/test/src/concrete/TestCloneFactory.t.sol new file mode 100644 index 0000000..30f830e --- /dev/null +++ b/test/src/concrete/TestCloneFactory.t.sol @@ -0,0 +1,126 @@ +// 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 {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; + +/// @title TestCloneFactoryTest +/// @notice `TestCloneFactory` is the executable stand-in for the real +/// `CloneFactory` in rain.factory.deploy, and the claim it exists to prove is +/// STRUCTURAL: the library surface suffices for a concrete that adds no +/// behaviour of its own. The 22 flow tests use it as a means to reach the +/// library; nothing asserted the delegation property itself, so a concrete +/// that quietly added behaviour — or that transposed two same-typed arguments +/// on the way through — would still have looked fine. +/// +/// This pins it: each of the four entry points equals the library function it +/// claims to be, computed independently here, and the two predictions really +/// are `view`. +contract TestCloneFactoryTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The concrete is an `ICloneableFactoryV4`, and therefore also carries + /// `ICloneableFactoryV3`'s two functions. Asserted as a real cast rather + /// than left to the inheritance list. + function testImplementsICloneableFactoryV4() external view { + ICloneableFactoryV4 factory = ICloneableFactoryV4(address(I_CLONE_FACTORY)); + assertEq(address(factory), address(I_CLONE_FACTORY)); + } + + /// `predictDeterministicAddress` is the library function, argument for + /// argument. `implementation` and `deployer` are both `address` and sit in + /// different positions, so transposing them would compile silently; fuzzing + /// them independently is what makes this discriminating. + function testPredictDeterministicAddressIsPureDelegation(address implementation, bytes32 salt, address deployer) + external + view + { + assertEq( + I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer), + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), implementation, LibICloneableFactoryV4.effectiveSalt(deployer, salt) + ) + ); + } + + /// `predictDeterministicAddressOpenSalt` is the library function, argument + /// for argument. + function testPredictDeterministicAddressOpenSaltIsPureDelegation( + address implementation, + bytes memory data, + bytes32 salt + ) external view { + assertEq( + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt), + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), implementation, LibICloneableFactoryV4.effectiveOpenSalt(salt, data) + ) + ); + } + + /// Both predictions are `external view` as the interface declares: a + /// `staticcall` to each succeeds and returns the same answer the ordinary + /// call does. A concrete that widened either to `nonpayable` — adding + /// behaviour the interface forbids — would fail the staticcall. + function testPredictionsAreStatic(address implementation, bytes memory data, bytes32 salt, address deployer) + external + view + { + (bool okNamespaced, bytes memory namespaced) = address(I_CLONE_FACTORY).staticcall( + abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddress, (implementation, salt, deployer)) + ); + assertTrue(okNamespaced, "predictDeterministicAddress is not static"); + assertEq( + abi.decode(namespaced, (address)), + I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer) + ); + + (bool okOpen, bytes memory open) = address(I_CLONE_FACTORY).staticcall( + abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt, (implementation, data, salt)) + ); + assertTrue(okOpen, "predictDeterministicAddressOpenSalt is not static"); + assertEq( + abi.decode(open, (address)), + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt) + ); + } + + /// The two deploying entry points route to the two DIFFERENT library + /// derivations, each landing where that derivation says and nowhere else. + /// Together with the prediction tests this covers all four delegations and + /// pins that none of them is wired to the other's derivation. + function testCloneEntryPointsRouteToTheirOwnDerivation(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address namespaced = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + address open = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq( + namespaced, + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), + address(implementation), + LibICloneableFactoryV4.effectiveSalt(address(this), salt) + ) + ); + assertEq( + open, + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), + address(implementation), + LibICloneableFactoryV4.effectiveOpenSalt(salt, data) + ) + ); + } +} diff --git a/test/src/concrete/TestCloneableConformant.sol b/test/src/concrete/TestCloneableConformant.sol new file mode 100644 index 0000000..2320d6e --- /dev/null +++ b/test/src/concrete/TestCloneableConformant.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableConformant +/// @notice The `ICloneableV2` fixture that honours the interface's two +/// normative MUSTs, which `TestCloneable` does not: +/// +/// - `initialize` MUST NOT be callable more than once, so a second call +/// reverts instead of overwriting state; +/// - the RECOMMENDED typed overload MUST revert `InitializeSignatureFn` +/// always, so it is never accidentally called instead of the generic +/// `initialize(bytes)` that the factory calls. +/// +/// It exists so those obligations are executable rather than prose: +/// `TestCloneable` is deliberately the minimum a factory flow test needs and +/// satisfies neither. +contract TestCloneableConformant is ICloneableV2 { + /// Set once, by the first and only `initialize`. + bytes public sData; + + /// Whether `initialize` has already run. Set before the data so a + /// re-entrant call cannot slip past the guard. + bool public sInitialized; + + /// Thrown by a second `initialize`. + error AlreadyInitialized(); + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + if (sInitialized) { + revert AlreadyInitialized(); + } + sInitialized = true; + sData = data; + return ICLONEABLE_V2_SUCCESS; + } + + /// The RECOMMENDED typed overload of `initialize`, which exists only so + /// the initialization config type appears in the ABI. It MUST revert + /// always, per `ICloneableV2`. + /// @param value The typed config that a caller would otherwise have + /// passed. Never read. + function initialize(uint256 value) external pure returns (bytes32) { + value; + revert InitializeSignatureFn(); + } +} diff --git a/test/src/concrete/TestCloneableEmitter.sol b/test/src/concrete/TestCloneableEmitter.sol new file mode 100644 index 0000000..f9ca7f5 --- /dev/null +++ b/test/src/concrete/TestCloneableEmitter.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableEmitter +/// @notice An `ICloneableV2` that EMITS during `initialize`, and records every +/// call it receives. Neither `TestCloneable` nor `TestCloneableFailure` does +/// either, which leaves two clauses of the shared factory spec unobservable: +/// the ordering of `NewClone` against the clone's own initialization logs, and +/// the MUST NOT that no other function is called on the proxy before +/// `initialize`. +contract TestCloneableEmitter is ICloneableV2 { + /// Emitted from inside `initialize`, so a test can place it in the log + /// stream relative to `NewClone`. + /// @param data The initialization data as the clone received it. + event Initialized(bytes data); + + /// Emitted by the fallback, i.e. by ANY call that is not + /// `initialize(bytes)`. Its presence before `Initialized` would be a spec + /// violation by the factory. + /// @param callData The calldata of the unexpected call. + event UnexpectedCall(bytes callData); + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + emit Initialized(data); + return ICLONEABLE_V2_SUCCESS; + } + + /// Any call other than `initialize(bytes)` lands here and is recorded. + fallback() external { + emit UnexpectedCall(msg.data); + } +} diff --git a/test/src/concrete/TestCloneableReverter.sol b/test/src/concrete/TestCloneableReverter.sol new file mode 100644 index 0000000..a50403c --- /dev/null +++ b/test/src/concrete/TestCloneableReverter.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableReverter +/// @notice An `ICloneableV2` whose `initialize` REVERTS with its own typed +/// error carrying its own data. `TestCloneableFailure` returns a non-success +/// hash instead, so on its own the suite never distinguishes "initialize +/// failed the sentinel check" from "initialize reverted" — and never proves +/// that the implementation's own revert reaches the caller instead of being +/// flattened into the library's `InitializationFailed`. +contract TestCloneableReverter is ICloneableV2 { + /// Thrown unconditionally by `initialize`. + /// @param data The data the clone was initialized with, echoed back so a + /// test can prove the revert reason survives verbatim. + error InitializeReverted(bytes data); + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external pure returns (bytes32) { + revert InitializeReverted(data); + } +} diff --git a/test/src/concrete/TestCloneableV1Shaped.sol b/test/src/concrete/TestCloneableV1Shaped.sol new file mode 100644 index 0000000..e02432e --- /dev/null +++ b/test/src/concrete/TestCloneableV1Shaped.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; + +/// @title TestCloneableV1Shaped +/// @notice A legacy `ICloneableV1`: `initialize(bytes)` with NO return value. +/// Its selector is `initialize(bytes)` — byte for byte the same selector +/// `ICloneableV2` publishes — so a V4 factory will happily call it and only +/// the return arity distinguishes the two interfaces. The fixture exists so +/// that difference is exercised rather than assumed. +contract TestCloneableV1Shaped is ICloneableV1 { + /// Set by `initialize`, so a test can prove the call actually landed. + bytes public sData; + + /// @inheritdoc ICloneableV1 + function initialize(bytes memory data) external { + sData = data; + } +} diff --git a/test/src/interface/ICloneableFactoryV2.sol.t.sol b/test/src/interface/ICloneableFactoryV2.sol.t.sol new file mode 100644 index 0000000..d1ceb51 --- /dev/null +++ b/test/src/interface/ICloneableFactoryV2.sol.t.sol @@ -0,0 +1,71 @@ +// 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 {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; +import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; + +/// @title ICloneableFactoryV2DeclarationTest +/// @notice `ICloneableFactoryV2` is the legacy, nonce-dependent factory +/// interface. Nothing in `src/` or `test/` imports it and it has no logic, but +/// it is PUBLISHED: it ships in the soldeer package and consumers pinned to it +/// compile against these exact bytes. Declaration-only is not the same as +/// unowned, so the surface is pinned here rather than left to drift silently. +contract ICloneableFactoryV2DeclarationTest is Test { + /// The legacy `clone` selector. + function testCloneSelectorPinned() external pure { + assertEq(ICloneableFactoryV2.clone.selector, bytes4(keccak256("clone(address,bytes)"))); + } + + /// The legacy `NewClone` topic. + function testNewCloneTopicZeroPinned() external pure { + assertEq(ICloneableFactoryV2.NewClone.selector, keccak256("NewClone(address,address,address)")); + } + + /// A PUBLISHED-SURFACE HAZARD, pinned as a fact rather than asserted away. + /// Three of the four factory interfaces in this package declare an event + /// called `NewClone`. `ICloneableFactoryV1` and `ICloneableFactoryV2` + /// declare the SAME three-parameter signature, so they are literally the + /// same topic and an indexer cannot tell a V1 clone from a V2 clone by + /// `topics[0]`. `ICloneableFactoryV3` takes five parameters and is a + /// different topic entirely. Any change to these relationships is a + /// breaking change for every downstream indexer. + function testNewCloneTopicRelationshipsAcrossTheFamily() external pure { + assertEq(ICloneableFactoryV1.NewClone.selector, ICloneableFactoryV2.NewClone.selector); + assertTrue(ICloneableFactoryV2.NewClone.selector != ICloneableFactoryV3.NewClone.selector); + } + + /// The declaration itself, as published: three unindexed `address` + /// parameters in the order sender, implementation, clone; and + /// `clone(address implementation, bytes data)` returning an unnamed + /// `address`. + function testAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV2", "ICloneableFactoryV2"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "NewClone: sender, implementation, clone - in that order, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "clone(implementation, data) returns address" + ); + } +} diff --git a/test/src/interface/ICloneableFactoryV3.sol.t.sol b/test/src/interface/ICloneableFactoryV3.sol.t.sol new file mode 100644 index 0000000..4a0cbbd --- /dev/null +++ b/test/src/interface/ICloneableFactoryV3.sol.t.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; +import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; +import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; + +/// @title ICloneableFactoryV3DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableFactoryV3` — the +/// `NewClone` event and the two function signatures — as a consumer sees it. +/// +/// The flow tests already assert `topics[0]` and the whole log data blob, and +/// that is enough for anything the EVM can see. It is NOT enough for the +/// event's actual contract, which is that "an indexer can reconstruct the +/// deploy from the event alone": an indexer decodes the five fields BY NAME +/// from the ABI. Swapping two of the three `address` parameters in the +/// declaration leaves every topic and every data byte identical while telling +/// every consumer that the deployer is the clone — so it is the artifact, not +/// the log, that has to be pinned. +contract ICloneableFactoryV3DeclarationTest is Test { + /// `NewClone`'s wire identity: the five-parameter signature, hashed. This + /// is what an indexer subscribes to, and it is deliberately restated from + /// the literal string rather than read back off the event. + function testNewCloneTopicZeroPinned() external pure { + assertEq( + ICloneableFactoryV3.NewClone.selector, keccak256("NewClone(address,address,address,bytes32,bytes)") + ); + } + + /// `ICloneableFactoryV3.NewClone` and `ICloneableFactoryV2.NewClone` share + /// a NAME across the published interface family and are DIFFERENT events: + /// five parameters against three, and therefore different `topics[0]`. + /// Pinned as a fact of the published surface — an indexer keyed on + /// `topics[0]` separates them, one keyed on the name alone does not. + function testNewCloneNameIsSharedAcrossTheInterfaceFamily() external pure { + assertTrue(ICloneableFactoryV3.NewClone.selector != ICloneableFactoryV2.NewClone.selector); + assertEq(ICloneableFactoryV2.NewClone.selector, keccak256("NewClone(address,address,address)")); + } + + /// The two function selectors of the V3 surface. + function testFunctionSelectorsPinned() external pure { + assertEq(ICloneableFactoryV3.cloneDeterministic.selector, bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)"))); + assertEq( + ICloneableFactoryV3.predictDeterministicAddress.selector, + bytes4(keccak256("predictDeterministicAddress(address,bytes32,address)")) + ); + } + + /// THE DECLARATION ITSELF. The five parameters in order, each with its + /// published name and type, none of them `indexed`, and the event not + /// anonymous. Written out as the ABI a consumer downloads, so a reorder or + /// a rename — neither of which any log assertion can see — fails here. + function testNewCloneAbiPinned() external view { + assertTrue( + vm.contains( + LibPublishedAbi.artifactJson("ICloneableFactoryV3", "ICloneableFactoryV3"), + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}" + "],\"anonymous\":false}" + ), + "NewClone: sender, implementation, clone, salt, data - in that order, none indexed" + ); + } + + /// The V3 function declarations, as published. + function testFunctionAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV3", "ICloneableFactoryV3"); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"cloneDeterministic\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "cloneDeterministic(implementation, data, salt) returns address" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"predictDeterministicAddress\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}," + "{\"name\":\"deployer\",\"type\":\"address\",\"internalType\":\"address\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"view\"}" + ), + "predictDeterministicAddress(implementation, salt, deployer) returns address, view" + ); + } + + /// `NewClone` is SHARED IDENTICALLY by both entry points, not merely + /// emitted by each: the same `topics[0]`, the same topic count, and the + /// same data layout come out of `cloneDeterministic` and + /// `cloneDeterministicOpenSalt` for the same caller, implementation, salt + /// and data. Asserted against each other in one test, so a change that + /// forked the two would fail here even if it kept each entry point + /// self-consistent. + function testNewCloneSharedByBothEntryPoints(bytes32 salt, bytes memory data) external { + TestCloneFactory factory = new TestCloneFactory(); + TestCloneable implementation = new TestCloneable(); + + uint256 snapshot = vm.snapshotState(); + + vm.recordLogs(); + address namespacedChild = factory.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory namespacedLogs = vm.getRecordedLogs(); + + vm.revertToState(snapshot); + + vm.recordLogs(); + address openChild = factory.cloneDeterministicOpenSalt(address(implementation), data, salt); + Vm.Log[] memory openLogs = vm.getRecordedLogs(); + + assertEq(namespacedLogs.length, 1); + assertEq(openLogs.length, 1); + assertEq(namespacedLogs[0].topics.length, 1); + assertEq(openLogs[0].topics.length, 1); + assertEq(namespacedLogs[0].topics[0], openLogs[0].topics[0]); + assertEq(namespacedLogs[0].topics[0], ICloneableFactoryV3.NewClone.selector); + + // The two derivations put the clone at different addresses, so the + // blobs differ in exactly that one field and nowhere else. + assertTrue(namespacedChild != openChild); + assertEq(namespacedLogs[0].data, abi.encode(address(this), address(implementation), namespacedChild, salt, data)); + assertEq(openLogs[0].data, abi.encode(address(this), address(implementation), openChild, salt, data)); + } +} diff --git a/test/src/interface/ICloneableV2.sol.t.sol b/test/src/interface/ICloneableV2.sol.t.sol new file mode 100644 index 0000000..231d4cf --- /dev/null +++ b/test/src/interface/ICloneableV2.sol.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; +import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; +import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; +import {TestCloneableConformant} from "test/src/concrete/TestCloneableConformant.sol"; +import {TestCloneableEmitter} from "test/src/concrete/TestCloneableEmitter.sol"; +import {TestCloneableReverter} from "test/src/concrete/TestCloneableReverter.sol"; + +/// @title ICloneableV2DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableV2` and makes its two +/// normative MUSTs executable. +/// +/// `initialize`'s selector is compile-guarded — retype or rename it and the +/// library stops compiling — but `InitializeSignatureFn` is not: nothing in +/// this repo referenced it before this file, so its selector could drift +/// freely while every test stayed green. It is the error every conforming +/// implementation is required to revert with, so its selector is exactly the +/// kind of value a downstream consumer decodes. +contract ICloneableV2DeclarationTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The generic entry point's selector — what `cloneAndInitialize` calls. + function testInitializeSelectorPinned() external pure { + assertEq(ICloneableV2.initialize.selector, bytes4(keccak256("initialize(bytes)"))); + } + + /// `ICloneableV1.initialize` and `ICloneableV2.initialize` publish the + /// SAME selector and differ only in return arity — the whole of the V1/V2 + /// difference, and the reason a V4 factory cannot tell one from the other + /// before calling it. + function testInitializeSelectorSharedWithV1() external pure { + assertEq(ICloneableV1.initialize.selector, ICloneableV2.initialize.selector); + } + + /// The error every conforming implementation's typed overload must revert + /// with, pinned from its literal signature. + function testInitializeSignatureFnSelectorPinned() external pure { + assertEq(ICloneableV2.InitializeSignatureFn.selector, bytes4(keccak256("InitializeSignatureFn()"))); + } + + /// The success sentinel is the hash of the documented string. + function testSuccessSentinelPinned() external pure { + assertEq(ICLONEABLE_V2_SUCCESS, keccak256("ICloneableV2.initialize")); + } + + /// THE DECLARATION ITSELF: `initialize(bytes data)` returning a named + /// `bytes32 success`, and a zero-parameter `InitializeSignatureFn` error. + /// Return types and parameter names are outside the selector, so this is + /// the only place they are pinned. + function testAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableV2", "ICloneableV2"); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"success\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "initialize(bytes data) returns (bytes32 success)" + ); + assertTrue( + vm.contains(json, "{\"type\":\"error\",\"name\":\"InitializeSignatureFn\",\"inputs\":[]}"), + "error InitializeSignatureFn() takes no parameters" + ); + } + + /// MUST: `initialize` can NOT be called more than once. Exercised end to + /// end on a clone the factory just produced, so the guard is proven where + /// it matters — after the factory's own atomic initialization has already + /// consumed the one permitted call. The stored data is unchanged by the + /// rejected second call. + function testInitializeOnlyOnce(bytes32 salt, bytes memory data, bytes memory otherData) external { + TestCloneableConformant implementation = new TestCloneableConformant(); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(TestCloneableConformant(child).sData(), data); + + vm.expectRevert(abi.encodeWithSelector(TestCloneableConformant.AlreadyInitialized.selector)); + TestCloneableConformant(child).initialize(otherData); + + assertEq(TestCloneableConformant(child).sData(), data); + } + + /// MUST: a typed overload of `initialize` reverts `InitializeSignatureFn` + /// always, so it is never accidentally called in place of the generic + /// `initialize(bytes)` the factory calls. Asserted on a clone, uninitialized + /// and initialized alike — "always" means both. + function testTypedOverloadRevertsInitializeSignatureFn(bytes32 salt, bytes memory data, uint256 value) external { + TestCloneableConformant implementation = new TestCloneableConformant(); + + vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); + implementation.initialize(value); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); + TestCloneableConformant(child).initialize(value); + } + + /// A revert inside `initialize` reaches the caller VERBATIM — the + /// implementation's own typed error with its own data — rather than being + /// flattened into the library's `InitializationFailed`, which is reserved + /// for the case where `initialize` RETURNS the wrong sentinel. Nothing is + /// deployed at the address either way. + function testInitializeRevertBubblesVerbatim(bytes32 salt, bytes memory data) external { + TestCloneableReverter implementation = new TestCloneableReverter(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(abi.encodeWithSelector(TestCloneableReverter.InitializeReverted.selector, data)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } + + /// The factory calls `initialize(bytes)` and NOTHING ELSE on the fresh + /// proxy, and `NewClone` is the FIRST log of the deploy. Both clauses are + /// only observable through an implementation that emits, which is why this + /// fixture exists: the log stream is exactly `NewClone` then the clone's + /// own `Initialized`, with no `UnexpectedCall` anywhere, and the data the + /// clone saw is the data the caller passed, byte for byte. + function testNothingCalledBeforeInitialize(bytes32 salt, bytes memory data) external { + TestCloneableEmitter implementation = new TestCloneableEmitter(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 2, "exactly NewClone and the clone's own Initialized"); + + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], keccak256("NewClone(address,address,address,bytes32,bytes)")); + + assertEq(entries[1].emitter, child, "the second log is the clone's own"); + assertEq(entries[1].topics[0], keccak256("Initialized(bytes)")); + assertEq(entries[1].data, abi.encode(data), "initialize received the caller's data verbatim"); + } +} diff --git a/test/src/interface/deprecated/DeprecatedInterfaces.t.sol b/test/src/interface/deprecated/DeprecatedInterfaces.t.sol new file mode 100644 index 0000000..a34b4c0 --- /dev/null +++ b/test/src/interface/deprecated/DeprecatedInterfaces.t.sol @@ -0,0 +1,118 @@ +// 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 {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; +import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; +import {IFactory} from "src/interface/deprecated/IFactory.sol"; +import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; + +/// @title DeprecatedInterfacesDeclarationTest +/// @notice `src/interface/deprecated/` is imported by nothing in this repo and +/// carries no logic, but it SHIPS in the soldeer package: contracts deployed +/// years ago are still described by these ABIs and indexers still decode +/// against them. Deprecated means "do not use for new work", not "free to +/// change" — a deprecated declaration that drifts silently breaks consumers +/// that cannot be redeployed. Everything here is therefore pinned. +contract DeprecatedInterfacesDeclarationTest is Test { + /// `ICloneableV1.initialize` has NO return value. That is the entire + /// V1/V2 difference, and it is invisible in the selector, so the ABI is + /// the only place it can be pinned. + function testICloneableV1AbiPinned() external view { + assertEq(ICloneableV1.initialize.selector, bytes4(keccak256("initialize(bytes)"))); + assertTrue( + vm.contains( + LibPublishedAbi.artifactJson("ICloneableV1", "ICloneableV1"), + "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[],\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableV1.initialize(bytes) returns NOTHING" + ); + } + + /// `ICloneableFactoryV1`: the three-parameter `NewClone` and `clone`. + function testICloneableFactoryV1AbiPinned() external view { + assertEq(ICloneableFactoryV1.NewClone.selector, keccak256("NewClone(address,address,address)")); + assertEq(ICloneableFactoryV1.clone.selector, bytes4(keccak256("clone(address,bytes)"))); + + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV1", "ICloneableFactoryV1"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "ICloneableFactoryV1.NewClone: sender, implementation, clone, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableFactoryV1.clone(implementation, data) returns address" + ); + } + + /// `IFactory`: the two events and the two functions. `isChild` returning + /// `bool` is load-bearing — the interface calls it CRITICAL to the + /// security guarantees of any implementation — and a return type is not + /// part of a selector, so only the ABI pins it. + function testIFactoryAbiPinned() external view { + assertEq(IFactory.NewChild.selector, keccak256("NewChild(address,address)")); + assertEq(IFactory.Implementation.selector, keccak256("Implementation(address,address)")); + assertEq(IFactory.createChild.selector, bytes4(keccak256("createChild(bytes)"))); + assertEq(IFactory.isChild.selector, bytes4(keccak256("isChild(address)"))); + + string memory json = LibPublishedAbi.artifactJson("IFactory", "IFactory"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewChild\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"child\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.NewChild: sender, child, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"Implementation\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.Implementation: sender, implementation, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"createChild\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "IFactory.createChild(data) returns address" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"isChild\",\"inputs\":[" + "{\"name\":\"maybeChild\",\"type\":\"address\",\"internalType\":\"address\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}]," + "\"stateMutability\":\"view\"}" + ), + "IFactory.isChild(maybeChild) returns bool, view" + ); + } +} diff --git a/test/src/lib/LibPublishedAbi.sol b/test/src/lib/LibPublishedAbi.sol new file mode 100644 index 0000000..f27b764 --- /dev/null +++ b/test/src/lib/LibPublishedAbi.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Vm} from "forge-std-1.16.1/src/Vm.sol"; + +/// @dev The `Vm` address, as forge-std computes it. Duplicated here rather than +/// inherited from `Test` so this helper is usable from a plain library. +Vm constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + +/// @title LibPublishedAbi +/// @notice Reads a compiled artifact's `abi` array so a test can pin what the +/// EVM cannot see. +/// +/// Everything in `src/interface` is a PUBLISHED surface: downstream indexers +/// and soldeer consumers decode `NewClone` and call `initialize` from the ABI, +/// BY NAME. Four things in a declaration are consumer-visible but completely +/// invisible on chain: +/// +/// - parameter NAMES, +/// - the ORDER of parameters that share a type (swapping two `address` event +/// parameters changes no topic, no log data byte and no selector), +/// - `indexed` flags on parameters that are not currently indexed, +/// - RETURN types (they are not part of a function selector). +/// +/// A log-level or selector-level assertion cannot discriminate any of them, so +/// the compiled artifact is the only oracle. `forge test` recompiles before it +/// runs, so the artifact read here is never stale with respect to the source +/// under test. +library LibPublishedAbi { + /// The full artifact JSON for `.sol/.json` under `out`. + /// @param file The solidity file name, without extension. + /// @param name The contract or interface name within it. + /// @return The artifact JSON. + function artifactJson(string memory file, string memory name) internal view returns (string memory) { + return VM.readFile(string.concat("out/", file, ".sol/", name, ".json")); + } +} From 3afc5b229bd0071f6556be3962d54e21c740169f Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 07:59:56 +0000 Subject: [PATCH 2/7] AMT g4: forge fmt, and stop the checkImplementationCode fuzz drawing undeployable code testCheckImplementationCodeEtched fails on UNMUTATED source whenever the fuzzer draws code whose first byte is 0xEF: vm.etch reads that as an EIP-7702 delegation designator and rejects it. EIP-3541 forbids deploying such code at all, so it can never be an implementation's code on chain, and excluding it narrows the fuzz domain to inputs the property is about rather than weakening it - the guard only ever reads code LENGTH. Reported independently as #64 and #68; fixed here because a baseline that goes red on a fuzz draw makes every mutation verdict in this campaign unreliable. Co-Authored-By: Claude Opus 5 (1M context) --- test/src/concrete/TestCloneFactory.t.sol | 19 ++++++++----------- .../interface/ICloneableFactoryV3.sol.t.sol | 13 ++++++++----- ...bleFactoryV4.checkImplementationCode.t.sol | 8 ++++++++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/test/src/concrete/TestCloneFactory.t.sol b/test/src/concrete/TestCloneFactory.t.sol index 30f830e..bf9214b 100644 --- a/test/src/concrete/TestCloneFactory.t.sol +++ b/test/src/concrete/TestCloneFactory.t.sol @@ -77,22 +77,21 @@ contract TestCloneFactoryTest is Test { external view { - (bool okNamespaced, bytes memory namespaced) = address(I_CLONE_FACTORY).staticcall( - abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddress, (implementation, salt, deployer)) - ); + (bool okNamespaced, bytes memory namespaced) = address(I_CLONE_FACTORY) + .staticcall(abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddress, (implementation, salt, deployer))); assertTrue(okNamespaced, "predictDeterministicAddress is not static"); assertEq( abi.decode(namespaced, (address)), I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer) ); - (bool okOpen, bytes memory open) = address(I_CLONE_FACTORY).staticcall( - abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt, (implementation, data, salt)) - ); + (bool okOpen, bytes memory open) = address(I_CLONE_FACTORY) + .staticcall( + abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt, (implementation, data, salt)) + ); assertTrue(okOpen, "predictDeterministicAddressOpenSalt is not static"); assertEq( - abi.decode(open, (address)), - I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt) + abi.decode(open, (address)), I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt) ); } @@ -117,9 +116,7 @@ contract TestCloneFactoryTest is Test { assertEq( open, LibICloneableFactoryV4.predictCloneAddress( - address(I_CLONE_FACTORY), - address(implementation), - LibICloneableFactoryV4.effectiveOpenSalt(salt, data) + address(I_CLONE_FACTORY), address(implementation), LibICloneableFactoryV4.effectiveOpenSalt(salt, data) ) ); } diff --git a/test/src/interface/ICloneableFactoryV3.sol.t.sol b/test/src/interface/ICloneableFactoryV3.sol.t.sol index 4a0cbbd..b6e3719 100644 --- a/test/src/interface/ICloneableFactoryV3.sol.t.sol +++ b/test/src/interface/ICloneableFactoryV3.sol.t.sol @@ -27,9 +27,7 @@ contract ICloneableFactoryV3DeclarationTest is Test { /// is what an indexer subscribes to, and it is deliberately restated from /// the literal string rather than read back off the event. function testNewCloneTopicZeroPinned() external pure { - assertEq( - ICloneableFactoryV3.NewClone.selector, keccak256("NewClone(address,address,address,bytes32,bytes)") - ); + assertEq(ICloneableFactoryV3.NewClone.selector, keccak256("NewClone(address,address,address,bytes32,bytes)")); } /// `ICloneableFactoryV3.NewClone` and `ICloneableFactoryV2.NewClone` share @@ -44,7 +42,10 @@ contract ICloneableFactoryV3DeclarationTest is Test { /// The two function selectors of the V3 surface. function testFunctionSelectorsPinned() external pure { - assertEq(ICloneableFactoryV3.cloneDeterministic.selector, bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)"))); + assertEq( + ICloneableFactoryV3.cloneDeterministic.selector, + bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)")) + ); assertEq( ICloneableFactoryV3.predictDeterministicAddress.selector, bytes4(keccak256("predictDeterministicAddress(address,bytes32,address)")) @@ -133,7 +134,9 @@ contract ICloneableFactoryV3DeclarationTest is Test { // The two derivations put the clone at different addresses, so the // blobs differ in exactly that one field and nowhere else. assertTrue(namespacedChild != openChild); - assertEq(namespacedLogs[0].data, abi.encode(address(this), address(implementation), namespacedChild, salt, data)); + assertEq( + namespacedLogs[0].data, abi.encode(address(this), address(implementation), namespacedChild, salt, data) + ); assertEq(openLogs[0].data, abi.encode(address(this), address(implementation), openChild, salt, data)); } } diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index 07f8ce4..7cafe80 100644 --- a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -38,6 +38,14 @@ contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { vm.assume(implementation.code.length == 0); vm.assume(uint160(implementation) > 0x0a); vm.assume(code.length > 0); + // EIP-3541 forbids DEPLOYING any code whose first byte is 0xEF, so no + // implementation on chain can have such code and `vm.etch` refuses to + // fabricate it (it reads a leading 0xEF as an EIP-7702 delegation + // designator and demands 23 bytes). The exclusion narrows the fuzz + // domain to code that could actually exist, which is what the guard is + // about; it does not weaken the property, since the guard only ever + // looks at code LENGTH. + vm.assume(code[0] != 0xEF); vm.etch(implementation, code); LibICloneableFactoryV4.checkImplementationCode(implementation); } From c89c653005e1cf14e0d2c727b0d29e8f82c9d3fa Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 12:17:35 +0000 Subject: [PATCH 3/7] test: move the ICloneableV2/factory fixtures out of the src mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test/src/**` mirrors `src/**` and holds the `.t.sol` suites. This repo is the library half of the split — there is no `src/concrete/`, the concrete lives in rain.factory.deploy — so `test/src/concrete/` mirrored nothing. Test SUPPORT code (harnesses, mocks, fixtures) belongs outside the mirror, in `test/concrete/`, `test/lib/`, `test/abstract/`, as in rain.deploy and rain.math.float. Pure move plus the import paths that follow it. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) --- test/{src => }/concrete/TestCloneFactory.sol | 0 test/{src => }/concrete/TestCloneable.sol | 0 test/{src => }/concrete/TestCloneableFailure.sol | 0 .../LibICloneableFactoryV4.checkImplementationCode.t.sol | 2 +- .../src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol | 6 +++--- .../LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol | 6 +++--- 6 files changed, 7 insertions(+), 7 deletions(-) rename test/{src => }/concrete/TestCloneFactory.sol (100%) rename test/{src => }/concrete/TestCloneable.sol (100%) rename test/{src => }/concrete/TestCloneableFailure.sol (100%) diff --git a/test/src/concrete/TestCloneFactory.sol b/test/concrete/TestCloneFactory.sol similarity index 100% rename from test/src/concrete/TestCloneFactory.sol rename to test/concrete/TestCloneFactory.sol diff --git a/test/src/concrete/TestCloneable.sol b/test/concrete/TestCloneable.sol similarity index 100% rename from test/src/concrete/TestCloneable.sol rename to test/concrete/TestCloneable.sol diff --git a/test/src/concrete/TestCloneableFailure.sol b/test/concrete/TestCloneableFailure.sol similarity index 100% rename from test/src/concrete/TestCloneableFailure.sol rename to test/concrete/TestCloneableFailure.sol diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index 07f8ce4..ace7b6d 100644 --- a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -5,7 +5,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibICloneableFactoryV4, ZeroImplementationCodeSize} from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; /// @title LibICloneableFactoryV4CheckImplementationCodeTest /// @notice Tests `LibICloneableFactoryV4.checkImplementationCode`: a codeless diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol index 8d82b5f..5a8e399 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -12,9 +12,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministic` / diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol index 1e59618..558637e 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -16,9 +16,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministicOpenSalt` / From 119b322a8c6c2dce74f5aa9c3cc614b0007e204a Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 12:20:40 +0000 Subject: [PATCH 4/7] test: one conforming ICloneableV2 fixture, and stop etching unrepresentable code Three parallel AMT branches (#76, #77, #78) each grew their own variant of `TestCloneable` because it satisfies neither of `ICloneableV2`'s normative MUSTs and moves in lockstep with the constant the library compares against. Fix it once, here, so the branches converge on one fixture instead of four: - `initialize` can NOT be called more than once. That is the interface's first MUST and no fixture honoured it. - The RECOMMENDED typed overload is present and reverts `InitializeSignatureFn` always, as the interface requires. - The success sentinel is written out from the LITERAL string the interface names, not imported from `ICLONEABLE_V2_SUCCESS`. Importing it put both sides of the library's comparison in lockstep: the constant could drift and every flow test would still pass, because the fixture drifted with it. A third party hard-codes `keccak256("ICloneableV2.initialize")`, so the fixture does too, and every existing flow test now discriminates a drift. Separately, `testCheckImplementationCodeEtched` could fail for a harness reason: `vm.etch` parses a `0xef01` prefix as an EIP-7702 delegation designator and rejects anything that is not exactly the 23-byte designator. EIP-3541 forbids deploying any `0xef`-leading code at all, so such code cannot exist at an implementation address on any chain and the guard is not specified over it; the fuzz domain is narrowed to code that could actually exist. The guard only ever reads code LENGTH, so nothing about the property changes. Gas snapshot regenerated for the extra `SSTORE` the initialization guard costs. Co-Authored-By: Claude Opus 5 (1M context) --- .gas-snapshot | 38 ++++++------ test/concrete/TestCloneable.sol | 60 +++++++++++++++++-- ...bleFactoryV4.checkImplementationCode.t.sol | 12 ++++ 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 8c76c86..923a59f 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,30 +1,30 @@ -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297933) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7939, ~: 7938) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7199, ~: 7267) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 459061, ~: 443058) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 466856, ~: 450600) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 553229, ~: 537270) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561146, ~: 544857) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 467536, ~: 451203) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 456513, ~: 438428) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 359037, ~: 349969) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 561754, ~: 545415) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 550725, ~: 532640) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 430975, ~: 421907) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6499, ~: 6482) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455295, ~: 438862) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361663, ~: 352503) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549507, ~: 533074) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433648, ~: 424488) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040442913, ~: 1040443151) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11124, ~: 11129) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 464734, ~: 448450) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 359015, ~: 349951) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445140, ~: 1040445380) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11112, ~: 11129) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 558991, ~: 542707) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 430953, ~: 421889) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455229, ~: 438823) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361175, ~: 352030) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549441, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433160, ~: 424015) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040443447, ~: 1040443612) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 459986, ~: 444001) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11086, ~: 11111) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445675, ~: 1040445852) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 554198, ~: 538213) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11088, ~: 11111) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) diff --git a/test/concrete/TestCloneable.sol b/test/concrete/TestCloneable.sol index 6fcf0a7..1d9cc0f 100644 --- a/test/concrete/TestCloneable.sol +++ b/test/concrete/TestCloneable.sol @@ -2,18 +2,68 @@ // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd pragma solidity =0.8.25; -import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// Thrown by a second call to `TestCloneable.initialize`. `ICloneableV2` says +/// the implementation MUST ensure `initialize` can NOT be called more than +/// once; this is how this fixture ensures it. +error TestCloneableAlreadyInitialized(); /// @title TestCloneable -/// @notice A cloneable contract that implements `ICloneableV2`. Initializes -/// whatever data is passed to `initialize` as `sData`. As `sData` is public, -/// we can easily test that it is set correctly. +/// @notice THE conforming `ICloneableV2` fixture. Every test that needs a +/// clone that initializes successfully uses this one, so there is a single +/// place where "what a correct `ICloneableV2` does" is written down, and every +/// flow test in the suite is run against something that actually honours the +/// interface rather than against the minimum the factory happens to check. +/// +/// Three properties, each load bearing: +/// +/// - It stores whatever `data` it was initialized with in the public `sData`, +/// so a test can prove the bytes reached the clone verbatim. +/// - `initialize` can NOT be called more than once — the interface's first +/// normative MUST. The flag is written before the data so a re-entrant call +/// cannot slip past the guard. +/// - It returns the success sentinel written out from the LITERAL STRING +/// `ICloneableV2` names, NOT the imported `ICLONEABLE_V2_SUCCESS`. Importing +/// the constant would put both sides of the library's comparison in +/// lockstep: change the constant and every clone still initializes, because +/// the fixture changed with it. A third party implementing `ICloneableV2` +/// has no such luxury — the interface tells them to return +/// `keccak256("ICloneableV2.initialize")` and they hard-code that value — so +/// the fixture hard-codes it too, and a drift in the constant surfaces as a +/// real `InitializationFailed` through a real factory. +/// +/// It also carries the RECOMMENDED typed overload, which the interface +/// requires to revert `InitializeSignatureFn` always. contract TestCloneable is ICloneableV2 { + /// The data this clone was initialized with. Set once. bytes public sData; + /// Whether `initialize` has already run on this clone. Storage lives on + /// the clone, not the implementation, because the factory reaches this + /// code through an EIP-1167 `DELEGATECALL` proxy. + bool public sInitialized; + /// @inheritdoc ICloneableV2 function initialize(bytes memory data) external returns (bytes32) { + if (sInitialized) { + revert TestCloneableAlreadyInitialized(); + } + sInitialized = true; sData = data; - return ICLONEABLE_V2_SUCCESS; + // Deliberately the literal, not `ICLONEABLE_V2_SUCCESS`. See the + // contract notice. + return keccak256("ICloneableV2.initialize"); + } + + /// The RECOMMENDED typed overload of `initialize`, which exists only so an + /// initialization config type appears in the ABI. `ICloneableV2` requires + /// it to revert `InitializeSignatureFn` ALWAYS, so that it is never + /// accidentally called in place of the generic `initialize(bytes)` the + /// factory calls. The parameter is unnamed because it is never read. + /// @return Never returns; the declared return type only exists so the + /// overload has the shape a real typed `initialize` would. + function initialize(uint256) external pure returns (bytes32) { + revert InitializeSignatureFn(); } } diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index ace7b6d..6e72b7a 100644 --- a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -38,6 +38,18 @@ contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { vm.assume(implementation.code.length == 0); vm.assume(uint160(implementation) > 0x0a); vm.assume(code.length > 0); + // EIP-3541 forbids DEPLOYING any code whose first byte is `0xef`, so + // no implementation on any chain can have such code and the guard is + // never specified over it. The exclusion narrows the fuzz domain to + // code that could actually exist at an address; it cannot weaken the + // property, because the guard only ever looks at code LENGTH. + // + // It is also what keeps this test from failing for a harness reason: + // `vm.etch` parses a `0xef01` prefix as an EIP-7702 delegation + // designator and rejects it unless the blob is exactly 23 bytes + // ("Eip7702 is not 23 bytes long"), so a fuzz run that drew one died + // in the cheatcode rather than in the code under test. + vm.assume(code[0] != 0xef); vm.etch(implementation, code); LibICloneableFactoryV4.checkImplementationCode(implementation); } From b80e493c60545ebc0a4fb70e116a82834be3fcc1 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 12:47:00 +0000 Subject: [PATCH 5/7] test: consolidate onto the shared fixtures, and lay the files out by what they test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge from `2026-08-24-test-fixtures-out-of-src-mirror` already relocated this branch's fixtures out of the `test/src` mirror, which only mirrors `src`. This finishes the job. Fixtures — three of this branch's five are gone, none of the coverage with them: - `TestCloneableConformant` was `TestCloneable` plus a one-shot init guard and the typed overload. The base branch gives `TestCloneable` both, for the whole suite rather than for two tests, so the second fixture had nothing left to add. `testInitializeOnlyOnce` and `testTypedOverloadRevertsInitializeSignatureFn` move onto it unchanged apart from the error name. - `TestCloneableReverter` and `TestCloneableEmitter` are replaced by `TestCloneableRevert` and `TestCloneableCallRecorder`, byte for byte the same files #78 carries, so the two branches merge without touching each other. - `TestCloneableRevert` and `TestCloneableFailure` stay SEPARATE. A fixture that returns a non-success sentinel cannot also revert with a typed error carrying data; collapsing them would weaken `testInitializeRevertBubblesVerbatim` to a bare decode revert. `testNothingCalledBeforeInitialize` gains a second, stronger oracle in the swap. `TestCloneableEmitter` announced a stray call by emitting from its fallback, so the whole assertion rode on a log count. `TestCloneableCallRecorder` records the SELECTOR SEQUENCE instead, and the test now asserts that exactly one call reached the clone and that it was `initialize(bytes)`. Mutation-checked: a `child.call(...)` inserted into `cloneAndInitialize` between `NewClone` and `initialize` is caught by the selector assertion ("2 != 1") and is INVISIBLE to the log-count assertion, because the recorder's fallback deliberately does not emit. `TestCloneableV1Shaped` was imported by nothing on this branch — a fixture built for a test that was never written. It now has it. `testInitializeSelectorSharedWithV1` pins that V1 and V2 publish the SAME `initialize(bytes)` selector, which is only half a hazard statement: the question it raises is whether a V4 factory silently accepts a legacy implementation and leaves a live clone that was never initialized. It does not — the `bytes32` return decode finds an empty returndata buffer and reverts before the sentinel comparison, with NO revert data, which the test asserts as observed behaviour rather than assuming `InitializationFailed`. Mutation-checked: give the fixture a `bytes32` return and the test fails. Layout, so a file name says what it tests: - `LibPublishedAbi.sol` is test support, not a mirror of any `src/lib` file, so it moves to `test/lib/`. - `ICloneableFactoryV2.sol.t.sol` and `ICloneableFactoryV3.sol.t.sol` lose the doubled extension. - `ICloneableV2.sol.t.sol` becomes `ICloneableV2.initialize.t.sol`, matching the `..t.sol` convention `test/src/lib` already uses. Every test in it is about `initialize` or its return sentinel. - `DeprecatedInterfaces.t.sol` splits into `ICloneableV1.t.sol`, `ICloneableFactoryV1.t.sol` and `IFactory.t.sol`, one per file in `src/interface/deprecated/`, which is what the mirror is for. The three tests were independent and shared nothing but the notice, which each file keeps. The `0xef` fuzz-domain narrowing this branch carried is resolved onto the base branch's version of the same line, which now holds it once for all four AMT branches instead of four times. Gas snapshot regenerated. Co-Authored-By: Claude Opus 5 (1M context) --- .gas-snapshot | 78 ++++++++---- test/concrete/TestCloneableCallRecorder.sol | 57 +++++++++ test/concrete/TestCloneableConformant.sol | 50 -------- test/concrete/TestCloneableEmitter.sol | 36 ------ test/concrete/TestCloneableRevert.sol | 32 +++++ test/concrete/TestCloneableReverter.sol | 24 ---- test/{src => }/lib/LibPublishedAbi.sol | 0 ...V2.sol.t.sol => ICloneableFactoryV2.t.sol} | 2 +- ...V3.sol.t.sol => ICloneableFactoryV3.t.sol} | 2 +- ...ol.t.sol => ICloneableV2.initialize.t.sol} | 83 ++++++++---- .../deprecated/DeprecatedInterfaces.t.sol | 118 ------------------ .../deprecated/ICloneableFactoryV1.t.sol | 49 ++++++++ .../interface/deprecated/ICloneableV1.t.sol | 35 ++++++ test/src/interface/deprecated/IFactory.t.sol | 72 +++++++++++ 14 files changed, 357 insertions(+), 281 deletions(-) create mode 100644 test/concrete/TestCloneableCallRecorder.sol delete mode 100644 test/concrete/TestCloneableConformant.sol delete mode 100644 test/concrete/TestCloneableEmitter.sol create mode 100644 test/concrete/TestCloneableRevert.sol delete mode 100644 test/concrete/TestCloneableReverter.sol rename test/{src => }/lib/LibPublishedAbi.sol (100%) rename test/src/interface/{ICloneableFactoryV2.sol.t.sol => ICloneableFactoryV2.t.sol} (98%) rename test/src/interface/{ICloneableFactoryV3.sol.t.sol => ICloneableFactoryV3.t.sol} (99%) rename test/src/interface/{ICloneableV2.sol.t.sol => ICloneableV2.initialize.t.sol} (64%) delete mode 100644 test/src/interface/deprecated/DeprecatedInterfaces.t.sol create mode 100644 test/src/interface/deprecated/ICloneableFactoryV1.t.sol create mode 100644 test/src/interface/deprecated/ICloneableV1.t.sol create mode 100644 test/src/interface/deprecated/IFactory.t.sol diff --git a/.gas-snapshot b/.gas-snapshot index 923a59f..dd01521 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,42 +1,70 @@ +ICloneableFactoryV1DeclarationTest:testAbiPinned() (gas: 10541) +ICloneableFactoryV2DeclarationTest:testAbiPinned() (gas: 10466) +ICloneableFactoryV2DeclarationTest:testCloneSelectorPinned() (gas: 290) +ICloneableFactoryV2DeclarationTest:testNewCloneTopicRelationshipsAcrossTheFamily() (gas: 270) +ICloneableFactoryV2DeclarationTest:testNewCloneTopicZeroPinned() (gas: 267) +ICloneableFactoryV3DeclarationTest:testFunctionAbiPinned() (gas: 15955) +ICloneableFactoryV3DeclarationTest:testFunctionSelectorsPinned() (gas: 325) +ICloneableFactoryV3DeclarationTest:testNewCloneAbiPinned() (gas: 13998) +ICloneableFactoryV3DeclarationTest:testNewCloneNameIsSharedAcrossTheInterfaceFamily() (gas: 313) +ICloneableFactoryV3DeclarationTest:testNewCloneSharedByBothEntryPoints(bytes32,bytes) (runs: 2048, μ: 1001798, ~: 980796) +ICloneableFactoryV3DeclarationTest:testNewCloneTopicZeroPinned() (gas: 245) +ICloneableV1DeclarationTest:testAbiPinned() (gas: 8133) +ICloneableV2InitializeTest:testAbiPinned() (gas: 11499) +ICloneableV2InitializeTest:testInitializeOnlyOnce(bytes32,bytes,bytes) (runs: 2048, μ: 437025, ~: 426854) +ICloneableV2InitializeTest:testInitializeRevertBubblesVerbatim(bytes32,bytes) (runs: 2048, μ: 186100, ~: 185874) +ICloneableV2InitializeTest:testInitializeSelectorPinned() (gas: 245) +ICloneableV2InitializeTest:testInitializeSelectorSharedWithV1() (gas: 267) +ICloneableV2InitializeTest:testInitializeSignatureFnSelectorPinned() (gas: 290) +ICloneableV2InitializeTest:testNothingCalledBeforeInitialize(bytes32,bytes) (runs: 2048, μ: 514516, ~: 503844) +ICloneableV2InitializeTest:testSuccessSentinelPinned() (gas: 289) +ICloneableV2InitializeTest:testTypedOverloadRevertsInitializeSignatureFn(bytes32,bytes,uint256) (runs: 2048, μ: 431188, ~: 421429) +ICloneableV2InitializeTest:testV1ShapedImplementationIsRejected(bytes32,bytes) (runs: 2048, μ: 352473, ~: 342027) +IFactoryDeclarationTest:testAbiPinned() (gas: 14452) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297933) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7939, ~: 7938) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7199, ~: 7267) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 553229, ~: 537270) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561146, ~: 544857) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 561754, ~: 545415) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 550725, ~: 532640) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 430975, ~: 421907) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 555838, ~: 537270) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 563836, ~: 544860) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8141, ~: 8126) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 563478, ~: 545418) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 553533, ~: 532640) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 432381, ~: 421907) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6499, ~: 6482) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549507, ~: 533074) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433648, ~: 424488) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445140, ~: 1040445380) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11112, ~: 11129) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 558991, ~: 542707) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 430953, ~: 421889) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6501, ~: 6482) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 552198, ~: 533074) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 435064, ~: 424488) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12250, ~: 12224) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445168, ~: 1040445384) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11113, ~: 11129) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561678, ~: 542710) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 432359, ~: 421889) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549441, ~: 533035) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433160, ~: 424015) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 552131, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 434576, ~: 424015) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445675, ~: 1040445852) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 554198, ~: 538213) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11088, ~: 11111) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445704, ~: 1040445864) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 556806, ~: 538213) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11089, ~: 11111) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) -LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1405, ~: 1398) +LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1406, ~: 1398) LibICloneableFactoryV4Test:testDomainTagsDistinct() (gas: 233) LibICloneableFactoryV4Test:testDomainTagsPinned() (gas: 325) -LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4687, ~: 4682) +LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4688, ~: 4682) LibICloneableFactoryV4Test:testEffectiveOpenSaltEmptyData(bytes32) (runs: 2048, μ: 826, ~: 826) -LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1269, ~: 1257) -LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1360, ~: 1348) -LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4177, ~: 4166) +LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1270, ~: 1257) +LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1361, ~: 1348) +LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4178, ~: 4166) LibICloneableFactoryV4Test:testEffectiveSaltDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) LibICloneableFactoryV4Test:testEffectiveSaltMatchesFormula(address,bytes32) (runs: 2048, μ: 865, ~: 865) LibICloneableFactoryV4Test:testEffectiveSaltPreimageShape(address,bytes32) (runs: 2048, μ: 966, ~: 966) -LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) \ No newline at end of file +LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) +TestCloneFactoryTest:testCloneEntryPointsRouteToTheirOwnDerivation(bytes32,bytes) (runs: 2048, μ: 553233, ~: 532270) +TestCloneFactoryTest:testImplementsICloneableFactoryV4() (gas: 306) +TestCloneFactoryTest:testPredictDeterministicAddressIsPureDelegation(address,bytes32,address) (runs: 2048, μ: 6132, ~: 6132) +TestCloneFactoryTest:testPredictDeterministicAddressOpenSaltIsPureDelegation(address,bytes,bytes32) (runs: 2048, μ: 7039, ~: 7020) +TestCloneFactoryTest:testPredictionsAreStatic(address,bytes,bytes32,address) (runs: 2048, μ: 13619, ~: 13590) \ No newline at end of file diff --git a/test/concrete/TestCloneableCallRecorder.sol b/test/concrete/TestCloneableCallRecorder.sol new file mode 100644 index 0000000..39d580d --- /dev/null +++ b/test/concrete/TestCloneableCallRecorder.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableCallRecorder +/// @notice An `ICloneableV2` that records the selector of every call the proxy +/// receives, in order, and emits a log of its own from inside `initialize`. +/// +/// `TestCloneable` exposes only the END STATE of a clone, so two clauses of +/// the shared factory spec are invisible through it: "MUST call `initialize` +/// ... MUST NOT call any other functions on the cloned proxy before +/// `initialize` completes successfully", and the ordering of the factory's +/// `NewClone` against the clone's own initialization. This fixture makes both +/// observable — `selectors()` is the whole call sequence, and `Initializing` +/// lands in the log stream at the moment `initialize` runs. +contract TestCloneableCallRecorder is ICloneableV2 { + /// Emitted from inside `initialize`, so the log stream orders the + /// factory's `NewClone` against the initialization call itself. + /// @param data The initialization data as the clone received it. + event Initializing(bytes data); + + /// Every selector the proxy has been called with, in order. Storage lives + /// on the clone, not the implementation, because the factory reaches this + /// code through an EIP-1167 `DELEGATECALL` proxy. + bytes4[] internal sSelectors; + + /// The data this clone was initialized with. + bytes public sData; + + /// The selectors recorded so far, in call order. + /// @return The recorded selectors. + function selectors() external view returns (bytes4[] memory) { + return sSelectors; + } + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + sSelectors.push(msg.sig); + sData = data; + emit Initializing(data); + // Deliberately the literal rather than `ICLONEABLE_V2_SUCCESS`, for + // the same reason `TestCloneable` writes it out: a fixture that + // imports the constant the library compares against moves in lockstep + // with it and cannot discriminate a drift. + return keccak256("ICloneableV2.initialize"); + } + + /// Records any other call the proxy receives, so a call the factory makes + /// before `initialize` cannot go unseen. Deliberately does not revert: a + /// factory that ignored the result of a stray call would otherwise leave + /// no trace at all. + fallback() external { + sSelectors.push(msg.sig); + } +} diff --git a/test/concrete/TestCloneableConformant.sol b/test/concrete/TestCloneableConformant.sol deleted file mode 100644 index 2320d6e..0000000 --- a/test/concrete/TestCloneableConformant.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; - -/// @title TestCloneableConformant -/// @notice The `ICloneableV2` fixture that honours the interface's two -/// normative MUSTs, which `TestCloneable` does not: -/// -/// - `initialize` MUST NOT be callable more than once, so a second call -/// reverts instead of overwriting state; -/// - the RECOMMENDED typed overload MUST revert `InitializeSignatureFn` -/// always, so it is never accidentally called instead of the generic -/// `initialize(bytes)` that the factory calls. -/// -/// It exists so those obligations are executable rather than prose: -/// `TestCloneable` is deliberately the minimum a factory flow test needs and -/// satisfies neither. -contract TestCloneableConformant is ICloneableV2 { - /// Set once, by the first and only `initialize`. - bytes public sData; - - /// Whether `initialize` has already run. Set before the data so a - /// re-entrant call cannot slip past the guard. - bool public sInitialized; - - /// Thrown by a second `initialize`. - error AlreadyInitialized(); - - /// @inheritdoc ICloneableV2 - function initialize(bytes memory data) external returns (bytes32) { - if (sInitialized) { - revert AlreadyInitialized(); - } - sInitialized = true; - sData = data; - return ICLONEABLE_V2_SUCCESS; - } - - /// The RECOMMENDED typed overload of `initialize`, which exists only so - /// the initialization config type appears in the ABI. It MUST revert - /// always, per `ICloneableV2`. - /// @param value The typed config that a caller would otherwise have - /// passed. Never read. - function initialize(uint256 value) external pure returns (bytes32) { - value; - revert InitializeSignatureFn(); - } -} diff --git a/test/concrete/TestCloneableEmitter.sol b/test/concrete/TestCloneableEmitter.sol deleted file mode 100644 index f9ca7f5..0000000 --- a/test/concrete/TestCloneableEmitter.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; - -/// @title TestCloneableEmitter -/// @notice An `ICloneableV2` that EMITS during `initialize`, and records every -/// call it receives. Neither `TestCloneable` nor `TestCloneableFailure` does -/// either, which leaves two clauses of the shared factory spec unobservable: -/// the ordering of `NewClone` against the clone's own initialization logs, and -/// the MUST NOT that no other function is called on the proxy before -/// `initialize`. -contract TestCloneableEmitter is ICloneableV2 { - /// Emitted from inside `initialize`, so a test can place it in the log - /// stream relative to `NewClone`. - /// @param data The initialization data as the clone received it. - event Initialized(bytes data); - - /// Emitted by the fallback, i.e. by ANY call that is not - /// `initialize(bytes)`. Its presence before `Initialized` would be a spec - /// violation by the factory. - /// @param callData The calldata of the unexpected call. - event UnexpectedCall(bytes callData); - - /// @inheritdoc ICloneableV2 - function initialize(bytes memory data) external returns (bytes32) { - emit Initialized(data); - return ICLONEABLE_V2_SUCCESS; - } - - /// Any call other than `initialize(bytes)` lands here and is recorded. - fallback() external { - emit UnexpectedCall(msg.data); - } -} diff --git a/test/concrete/TestCloneableRevert.sol b/test/concrete/TestCloneableRevert.sol new file mode 100644 index 0000000..af08396 --- /dev/null +++ b/test/concrete/TestCloneableRevert.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// Thrown unconditionally by `TestCloneableRevert.initialize`, carrying the +/// data it was called with so a test can prove both that the revert came from +/// the implementation rather than the factory, and that its arguments survive +/// the trip verbatim. +/// @param data The initialization data the clone was called with. +error TestCloneableRevertInitialize(bytes data); + +/// @title TestCloneableRevert +/// @notice An `ICloneableV2` whose `initialize` REVERTS, with its own typed +/// error carrying its own data. +/// +/// This is the other half of "initialization failed" from +/// `TestCloneableFailure`, and the two are not interchangeable. +/// `TestCloneableFailure` RETURNS a non-success value, which is the case the +/// library answers with its own `InitializationFailed`. This one REFUSES — +/// what a real implementation does when its `data` does not decode or its +/// invariants do not hold — and the library must let that revert through +/// untouched instead of flattening it. A fixture that returns cannot exercise +/// that, and a fixture that reverts cannot exercise the sentinel comparison, +/// so both exist. +contract TestCloneableRevert is ICloneableV2 { + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external pure returns (bytes32) { + revert TestCloneableRevertInitialize(data); + } +} diff --git a/test/concrete/TestCloneableReverter.sol b/test/concrete/TestCloneableReverter.sol deleted file mode 100644 index a50403c..0000000 --- a/test/concrete/TestCloneableReverter.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {ICloneableV2} from "src/interface/ICloneableV2.sol"; - -/// @title TestCloneableReverter -/// @notice An `ICloneableV2` whose `initialize` REVERTS with its own typed -/// error carrying its own data. `TestCloneableFailure` returns a non-success -/// hash instead, so on its own the suite never distinguishes "initialize -/// failed the sentinel check" from "initialize reverted" — and never proves -/// that the implementation's own revert reaches the caller instead of being -/// flattened into the library's `InitializationFailed`. -contract TestCloneableReverter is ICloneableV2 { - /// Thrown unconditionally by `initialize`. - /// @param data The data the clone was initialized with, echoed back so a - /// test can prove the revert reason survives verbatim. - error InitializeReverted(bytes data); - - /// @inheritdoc ICloneableV2 - function initialize(bytes memory data) external pure returns (bytes32) { - revert InitializeReverted(data); - } -} diff --git a/test/src/lib/LibPublishedAbi.sol b/test/lib/LibPublishedAbi.sol similarity index 100% rename from test/src/lib/LibPublishedAbi.sol rename to test/lib/LibPublishedAbi.sol diff --git a/test/src/interface/ICloneableFactoryV2.sol.t.sol b/test/src/interface/ICloneableFactoryV2.t.sol similarity index 98% rename from test/src/interface/ICloneableFactoryV2.sol.t.sol rename to test/src/interface/ICloneableFactoryV2.t.sol index d1ceb51..37e40cd 100644 --- a/test/src/interface/ICloneableFactoryV2.sol.t.sol +++ b/test/src/interface/ICloneableFactoryV2.t.sol @@ -7,7 +7,7 @@ import {Test} from "forge-std-1.16.1/src/Test.sol"; import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; -import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; /// @title ICloneableFactoryV2DeclarationTest /// @notice `ICloneableFactoryV2` is the legacy, nonce-dependent factory diff --git a/test/src/interface/ICloneableFactoryV3.sol.t.sol b/test/src/interface/ICloneableFactoryV3.t.sol similarity index 99% rename from test/src/interface/ICloneableFactoryV3.sol.t.sol rename to test/src/interface/ICloneableFactoryV3.t.sol index 91c10f7..3f5a81e 100644 --- a/test/src/interface/ICloneableFactoryV3.sol.t.sol +++ b/test/src/interface/ICloneableFactoryV3.t.sol @@ -6,7 +6,7 @@ import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; -import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; import {TestCloneable} from "test/concrete/TestCloneable.sol"; diff --git a/test/src/interface/ICloneableV2.sol.t.sol b/test/src/interface/ICloneableV2.initialize.t.sol similarity index 64% rename from test/src/interface/ICloneableV2.sol.t.sol rename to test/src/interface/ICloneableV2.initialize.t.sol index 97991c1..24bc15b 100644 --- a/test/src/interface/ICloneableV2.sol.t.sol +++ b/test/src/interface/ICloneableV2.initialize.t.sol @@ -6,15 +6,16 @@ import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; -import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; -import {TestCloneableConformant} from "test/concrete/TestCloneableConformant.sol"; -import {TestCloneableEmitter} from "test/concrete/TestCloneableEmitter.sol"; -import {TestCloneableReverter} from "test/concrete/TestCloneableReverter.sol"; - -/// @title ICloneableV2DeclarationTest -/// @notice Pins the PUBLISHED declaration of `ICloneableV2` and makes its two -/// normative MUSTs executable. +import {TestCloneable, TestCloneableAlreadyInitialized} from "test/concrete/TestCloneable.sol"; +import {TestCloneableRevert, TestCloneableRevertInitialize} from "test/concrete/TestCloneableRevert.sol"; +import {TestCloneableCallRecorder} from "test/concrete/TestCloneableCallRecorder.sol"; +import {TestCloneableV1Shaped} from "test/concrete/TestCloneableV1Shaped.sol"; + +/// @title ICloneableV2InitializeTest +/// @notice Pins the PUBLISHED declaration of `ICloneableV2.initialize` and +/// makes the interface's two normative MUSTs executable. /// /// `initialize`'s selector is compile-guarded — retype or rename it and the /// library stops compiling — but `InitializeSignatureFn` is not: nothing in @@ -22,7 +23,7 @@ import {TestCloneableReverter} from "test/concrete/TestCloneableReverter.sol"; /// freely while every test stayed green. It is the error every conforming /// implementation is required to revert with, so its selector is exactly the /// kind of value a downstream consumer decodes. -contract ICloneableV2DeclarationTest is Test { +contract ICloneableV2InitializeTest is Test { /// The `TestCloneFactory` instance under test. Stateless, so reused /// everywhere. TestCloneFactory internal immutable I_CLONE_FACTORY; @@ -83,15 +84,15 @@ contract ICloneableV2DeclarationTest is Test { /// consumed the one permitted call. The stored data is unchanged by the /// rejected second call. function testInitializeOnlyOnce(bytes32 salt, bytes memory data, bytes memory otherData) external { - TestCloneableConformant implementation = new TestCloneableConformant(); + TestCloneable implementation = new TestCloneable(); address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); - assertEq(TestCloneableConformant(child).sData(), data); + assertEq(TestCloneable(child).sData(), data); - vm.expectRevert(abi.encodeWithSelector(TestCloneableConformant.AlreadyInitialized.selector)); - TestCloneableConformant(child).initialize(otherData); + vm.expectRevert(abi.encodeWithSelector(TestCloneableAlreadyInitialized.selector)); + TestCloneable(child).initialize(otherData); - assertEq(TestCloneableConformant(child).sData(), data); + assertEq(TestCloneable(child).sData(), data); } /// MUST: a typed overload of `initialize` reverts `InitializeSignatureFn` @@ -99,7 +100,7 @@ contract ICloneableV2DeclarationTest is Test { /// `initialize(bytes)` the factory calls. Asserted on a clone, uninitialized /// and initialized alike — "always" means both. function testTypedOverloadRevertsInitializeSignatureFn(bytes32 salt, bytes memory data, uint256 value) external { - TestCloneableConformant implementation = new TestCloneableConformant(); + TestCloneable implementation = new TestCloneable(); vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); implementation.initialize(value); @@ -107,7 +108,7 @@ contract ICloneableV2DeclarationTest is Test { address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); - TestCloneableConformant(child).initialize(value); + TestCloneable(child).initialize(value); } /// A revert inside `initialize` reaches the caller VERBATIM — the @@ -116,36 +117,66 @@ contract ICloneableV2DeclarationTest is Test { /// for the case where `initialize` RETURNS the wrong sentinel. Nothing is /// deployed at the address either way. function testInitializeRevertBubblesVerbatim(bytes32 salt, bytes memory data) external { - TestCloneableReverter implementation = new TestCloneableReverter(); + TestCloneableRevert implementation = new TestCloneableRevert(); address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); - vm.expectRevert(abi.encodeWithSelector(TestCloneableReverter.InitializeReverted.selector, data)); + vm.expectRevert(abi.encodeWithSelector(TestCloneableRevertInitialize.selector, data)); I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); assertEq(predicted.code.length, 0); } /// The factory calls `initialize(bytes)` and NOTHING ELSE on the fresh - /// proxy, and `NewClone` is the FIRST log of the deploy. Both clauses are - /// only observable through an implementation that emits, which is why this - /// fixture exists: the log stream is exactly `NewClone` then the clone's - /// own `Initialized`, with no `UnexpectedCall` anywhere, and the data the - /// clone saw is the data the caller passed, byte for byte. + /// proxy, and `NewClone` is the FIRST log of the deploy. + /// + /// Two separate oracles, because the fixture carries two. The recorded + /// SELECTOR SEQUENCE is the direct evidence for the MUST NOT: exactly one + /// call reached the clone and it was `initialize(bytes)`, so a stray call + /// cannot hide in a log stream that happens to look right. The LOG STREAM + /// is what orders `NewClone` against the clone's own initialization, which + /// no amount of end-state inspection can show. function testNothingCalledBeforeInitialize(bytes32 salt, bytes memory data) external { - TestCloneableEmitter implementation = new TestCloneableEmitter(); + TestCloneableCallRecorder implementation = new TestCloneableCallRecorder(); vm.recordLogs(); address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 2, "exactly NewClone and the clone's own Initialized"); + bytes4[] memory selectors = TestCloneableCallRecorder(child).selectors(); + assertEq(selectors.length, 1, "exactly one call reached the clone"); + assertEq(selectors[0], ICloneableV2.initialize.selector, "and it was initialize(bytes)"); + + assertEq(entries.length, 2, "exactly NewClone and the clone's own Initializing"); assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); assertEq(entries[0].topics[0], keccak256("NewClone(address,address,address,bytes32,bytes)")); assertEq(entries[1].emitter, child, "the second log is the clone's own"); - assertEq(entries[1].topics[0], keccak256("Initialized(bytes)")); + assertEq(entries[1].topics[0], keccak256("Initializing(bytes)")); assertEq(entries[1].data, abi.encode(data), "initialize received the caller's data verbatim"); } + + /// The V1/V2 selector collision above is only half a hazard statement. A + /// V4 factory calls `initialize(bytes)` on whatever address it is handed, + /// and an `ICloneableV1` answers that selector — so the question the + /// collision raises is whether a legacy implementation gets SILENTLY + /// accepted, leaving a live clone that was never really initialized. + /// + /// It does not. `initialize` returns nothing on V1, so the factory's + /// decode of a `bytes32` return finds an empty returndata buffer and + /// reverts before it ever reaches the sentinel comparison. The revert + /// carries NO data — it is the ABI decoder, not a typed error — which is + /// asserted here as the actual observed behaviour rather than assumed to + /// be `InitializationFailed`. Nothing is deployed at the address. + function testV1ShapedImplementationIsRejected(bytes32 salt, bytes memory data) external { + TestCloneableV1Shaped implementation = new TestCloneableV1Shaped(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(bytes("")); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } } diff --git a/test/src/interface/deprecated/DeprecatedInterfaces.t.sol b/test/src/interface/deprecated/DeprecatedInterfaces.t.sol deleted file mode 100644 index a34b4c0..0000000 --- a/test/src/interface/deprecated/DeprecatedInterfaces.t.sol +++ /dev/null @@ -1,118 +0,0 @@ -// 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 {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; -import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; -import {IFactory} from "src/interface/deprecated/IFactory.sol"; -import {LibPublishedAbi} from "test/src/lib/LibPublishedAbi.sol"; - -/// @title DeprecatedInterfacesDeclarationTest -/// @notice `src/interface/deprecated/` is imported by nothing in this repo and -/// carries no logic, but it SHIPS in the soldeer package: contracts deployed -/// years ago are still described by these ABIs and indexers still decode -/// against them. Deprecated means "do not use for new work", not "free to -/// change" — a deprecated declaration that drifts silently breaks consumers -/// that cannot be redeployed. Everything here is therefore pinned. -contract DeprecatedInterfacesDeclarationTest is Test { - /// `ICloneableV1.initialize` has NO return value. That is the entire - /// V1/V2 difference, and it is invisible in the selector, so the ABI is - /// the only place it can be pinned. - function testICloneableV1AbiPinned() external view { - assertEq(ICloneableV1.initialize.selector, bytes4(keccak256("initialize(bytes)"))); - assertTrue( - vm.contains( - LibPublishedAbi.artifactJson("ICloneableV1", "ICloneableV1"), - "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" - "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" - "],\"outputs\":[],\"stateMutability\":\"nonpayable\"}" - ), - "ICloneableV1.initialize(bytes) returns NOTHING" - ); - } - - /// `ICloneableFactoryV1`: the three-parameter `NewClone` and `clone`. - function testICloneableFactoryV1AbiPinned() external view { - assertEq(ICloneableFactoryV1.NewClone.selector, keccak256("NewClone(address,address,address)")); - assertEq(ICloneableFactoryV1.clone.selector, bytes4(keccak256("clone(address,bytes)"))); - - string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV1", "ICloneableFactoryV1"); - assertTrue( - vm.contains( - json, - "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" - "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," - "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," - "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" - "],\"anonymous\":false}" - ), - "ICloneableFactoryV1.NewClone: sender, implementation, clone, none indexed" - ); - assertTrue( - vm.contains( - json, - "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" - "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," - "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" - "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," - "\"stateMutability\":\"nonpayable\"}" - ), - "ICloneableFactoryV1.clone(implementation, data) returns address" - ); - } - - /// `IFactory`: the two events and the two functions. `isChild` returning - /// `bool` is load-bearing — the interface calls it CRITICAL to the - /// security guarantees of any implementation — and a return type is not - /// part of a selector, so only the ABI pins it. - function testIFactoryAbiPinned() external view { - assertEq(IFactory.NewChild.selector, keccak256("NewChild(address,address)")); - assertEq(IFactory.Implementation.selector, keccak256("Implementation(address,address)")); - assertEq(IFactory.createChild.selector, bytes4(keccak256("createChild(bytes)"))); - assertEq(IFactory.isChild.selector, bytes4(keccak256("isChild(address)"))); - - string memory json = LibPublishedAbi.artifactJson("IFactory", "IFactory"); - assertTrue( - vm.contains( - json, - "{\"type\":\"event\",\"name\":\"NewChild\",\"inputs\":[" - "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," - "{\"name\":\"child\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" - "],\"anonymous\":false}" - ), - "IFactory.NewChild: sender, child, none indexed" - ); - assertTrue( - vm.contains( - json, - "{\"type\":\"event\",\"name\":\"Implementation\",\"inputs\":[" - "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," - "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" - "],\"anonymous\":false}" - ), - "IFactory.Implementation: sender, implementation, none indexed" - ); - assertTrue( - vm.contains( - json, - "{\"type\":\"function\",\"name\":\"createChild\",\"inputs\":[" - "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" - "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," - "\"stateMutability\":\"nonpayable\"}" - ), - "IFactory.createChild(data) returns address" - ); - assertTrue( - vm.contains( - json, - "{\"type\":\"function\",\"name\":\"isChild\",\"inputs\":[" - "{\"name\":\"maybeChild\",\"type\":\"address\",\"internalType\":\"address\"}" - "],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}]," - "\"stateMutability\":\"view\"}" - ), - "IFactory.isChild(maybeChild) returns bool, view" - ); - } -} diff --git a/test/src/interface/deprecated/ICloneableFactoryV1.t.sol b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol new file mode 100644 index 0000000..0184398 --- /dev/null +++ b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol @@ -0,0 +1,49 @@ +// 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 {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title ICloneableFactoryV1DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableFactoryV1`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract ICloneableFactoryV1DeclarationTest is Test { + /// `ICloneableFactoryV1`: the three-parameter `NewClone` and `clone`. + function testAbiPinned() external view { + assertEq(ICloneableFactoryV1.NewClone.selector, keccak256("NewClone(address,address,address)")); + assertEq(ICloneableFactoryV1.clone.selector, bytes4(keccak256("clone(address,bytes)"))); + + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV1", "ICloneableFactoryV1"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "ICloneableFactoryV1.NewClone: sender, implementation, clone, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableFactoryV1.clone(implementation, data) returns address" + ); + } +} diff --git a/test/src/interface/deprecated/ICloneableV1.t.sol b/test/src/interface/deprecated/ICloneableV1.t.sol new file mode 100644 index 0000000..d582c17 --- /dev/null +++ b/test/src/interface/deprecated/ICloneableV1.t.sol @@ -0,0 +1,35 @@ +// 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 {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title ICloneableV1DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableV1`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract ICloneableV1DeclarationTest is Test { + /// `ICloneableV1.initialize` has NO return value. That is the entire + /// V1/V2 difference, and it is invisible in the selector, so the ABI is + /// the only place it can be pinned. + function testAbiPinned() external view { + assertEq(ICloneableV1.initialize.selector, bytes4(keccak256("initialize(bytes)"))); + assertTrue( + vm.contains( + LibPublishedAbi.artifactJson("ICloneableV1", "ICloneableV1"), + "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[],\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableV1.initialize(bytes) returns NOTHING" + ); + } +} diff --git a/test/src/interface/deprecated/IFactory.t.sol b/test/src/interface/deprecated/IFactory.t.sol new file mode 100644 index 0000000..0f0cca3 --- /dev/null +++ b/test/src/interface/deprecated/IFactory.t.sol @@ -0,0 +1,72 @@ +// 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 {IFactory} from "src/interface/deprecated/IFactory.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title IFactoryDeclarationTest +/// @notice Pins the PUBLISHED declaration of `IFactory`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract IFactoryDeclarationTest is Test { + /// `IFactory`: the two events and the two functions. `isChild` returning + /// `bool` is load-bearing — the interface calls it CRITICAL to the + /// security guarantees of any implementation — and a return type is not + /// part of a selector, so only the ABI pins it. + function testAbiPinned() external view { + assertEq(IFactory.NewChild.selector, keccak256("NewChild(address,address)")); + assertEq(IFactory.Implementation.selector, keccak256("Implementation(address,address)")); + assertEq(IFactory.createChild.selector, bytes4(keccak256("createChild(bytes)"))); + assertEq(IFactory.isChild.selector, bytes4(keccak256("isChild(address)"))); + + string memory json = LibPublishedAbi.artifactJson("IFactory", "IFactory"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewChild\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"child\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.NewChild: sender, child, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"Implementation\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.Implementation: sender, implementation, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"createChild\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "IFactory.createChild(data) returns address" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"isChild\",\"inputs\":[" + "{\"name\":\"maybeChild\",\"type\":\"address\",\"internalType\":\"address\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}]," + "\"stateMutability\":\"view\"}" + ), + "IFactory.isChild(maybeChild) returns bool, view" + ); + } +} From 815c2eeb2dafa6a5def6e066a6e856dc8d204b8f Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 12:56:54 +0000 Subject: [PATCH 6/7] test: pin the one 0xef implementation code a real account can hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per CodeRabbit on this PR. The finding is correct and the comment on `vm.assume(code[0] != 0xef)` was overclaiming. The comment said no implementation on any chain can have `0xef`-leading code. EIP-3541 does forbid DEPLOYING it, so no CREATE or CREATE2 can produce it — but EIP-7702 leaves exactly one way an account can hold it anyway: a delegation designator, `0xef0100 || address`, exactly 23 bytes. `EXTCODESIZE` on a delegated EOA returns 23, not zero. The assume was silently excluding a case that is real, behind a comment saying it was not. `testCheckImplementationCodeEip7702Designator` pins it as a fixed case, since the fuzz test cannot reach it. It PASSES the guard, which is the part worth having on the record: a code-SIZE check cannot tell an implementation contract from an EOA that has delegated, and a delegation is REVOCABLE by the account holder where deployed code is not. Callers wanting an immutable implementation do not get that from this guard. Mutation-checked, not assumed: with the guard mutated to `code.length == 0 || code[0] == 0xef`, this is the ONLY test in the suite that fails (42 pass, 1 fail). The fuzz test cannot kill that mutant by construction, because its assume excludes the input that would. Scoped honestly in the NatSpec: `foundry.toml` pins `evm_version = "cancun"`, which predates EIP-7702, so the test asserts that the 23-byte designator is storable and passes the SIZE check. It does not exercise, and does not claim, the execution semantics of delegation. The rest of the assume stands: `0xef`-leading blobs of any OTHER length cannot exist on any chain, and `vm.etch` rejects them outright ("Eip7702 is not 23 bytes long"), which is what was breaking the fuzz test 8 runs in 8. Co-Authored-By: Claude Opus 5 (1M context) --- .gas-snapshot | 37 ++++++++-------- ...bleFactoryV4.checkImplementationCode.t.sol | 44 +++++++++++++++++-- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 923a59f..8333966 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,29 +1,30 @@ -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297933) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7939, ~: 7938) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7199, ~: 7267) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297955) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEip7702Designator(address,address) (runs: 2048, μ: 7422, ~: 7422) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7962, ~: 7960) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7169, ~: 7223) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 553229, ~: 537270) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561146, ~: 544857) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 553640, ~: 537270) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561371, ~: 544860) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 561754, ~: 545415) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 550725, ~: 532640) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 430975, ~: 421907) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 562154, ~: 545415) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 551175, ~: 532640) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 431200, ~: 421907) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6499, ~: 6482) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549507, ~: 533074) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433648, ~: 424488) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6500, ~: 6482) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 550042, ~: 533074) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433874, ~: 424488) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445140, ~: 1040445380) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445147, ~: 1040445384) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11112, ~: 11129) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 558991, ~: 542707) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 430953, ~: 421889) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 559215, ~: 542710) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 431178, ~: 421889) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549441, ~: 533035) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433160, ~: 424015) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549976, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433386, ~: 424015) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445675, ~: 1040445852) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 554198, ~: 538213) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445683, ~: 1040445864) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 554608, ~: 538213) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11088, ~: 11111) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index 6e72b7a..52b3d00 100644 --- a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -39,10 +39,16 @@ contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { vm.assume(uint160(implementation) > 0x0a); vm.assume(code.length > 0); // EIP-3541 forbids DEPLOYING any code whose first byte is `0xef`, so - // no implementation on any chain can have such code and the guard is - // never specified over it. The exclusion narrows the fuzz domain to - // code that could actually exist at an address; it cannot weaken the - // property, because the guard only ever looks at code LENGTH. + // no CREATE or CREATE2 can put such code at an address. That leaves + // exactly one way an account can hold it — an EIP-7702 delegation + // designator, which is `0xef0100` followed by an address and is + // therefore EXACTLY 23 bytes. That case is real, so it is not excluded + // here, it is pinned by its own test below. + // + // What this exclusion drops is the rest: `0xef`-leading blobs of any + // other length, which no chain can produce. It cannot weaken the + // property under test, because the guard only ever looks at code + // LENGTH. // // It is also what keeps this test from failing for a harness reason: // `vm.etch` parses a `0xef01` prefix as an EIP-7702 delegation @@ -53,4 +59,34 @@ contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { vm.etch(implementation, code); LibICloneableFactoryV4.checkImplementationCode(implementation); } + + /// The one `0xef`-leading code a real account can hold: an EIP-7702 + /// delegation designator, `0xef0100 || address`, exactly 23 bytes. The + /// fuzz test above cannot reach it, so it is pinned here as a fixed case. + /// + /// It PASSES the guard, and that is the point worth having on the record. + /// `EXTCODESIZE` on a delegated EOA returns 23, not zero, so the size + /// check cannot tell an ordinary implementation contract from an EOA that + /// has delegated — and unlike a deployed contract, a delegation is + /// REVOCABLE by the account holder at any time. A caller who wants an + /// immutable implementation does not get that from this guard; the guard + /// promises only that something is there. + /// + /// Scoped honestly: `foundry.toml` pins `evm_version = "cancun"`, which + /// predates EIP-7702, so what is asserted here is that the 23-byte + /// designator is storable at an address and passes the SIZE check. The + /// execution semantics of delegation are not exercised and this test does + /// not claim them. + function testCheckImplementationCodeEip7702Designator(address delegated, address delegate) external { + vm.assume(delegated.code.length == 0); + vm.assume(uint160(delegated) > 0x0a); + + bytes memory designator = abi.encodePacked(hex"ef0100", delegate); + assertEq(designator.length, 23, "an EIP-7702 designator is 23 bytes"); + + vm.etch(delegated, designator); + + assertEq(delegated.code.length, 23, "EXTCODESIZE sees the designator, not zero"); + LibICloneableFactoryV4.checkImplementationCode(delegated); + } } From cd4e60e593a6a8e9b0420834ebc176f695d73602 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Mon, 24 Aug 2026 13:03:34 +0000 Subject: [PATCH 7/7] test: keep the deprecated ABI test names the QA record names The split into one file per deprecated interface made `testICloneableV1AbiPinned` and friends look redundant against their new contract names, so the previous commit shortened all three to `testAbiPinned`. That was wrong twice over. The PR's `## QA` block names those three tests as the killers of M45, M41, M42, M43, M36 and M46. That block is a record of a mutation run that actually happened; renaming its subjects makes it unfollowable, and editing it to match would be rewriting the evidence rather than keeping it true. The short name also collided. Two `testAbiPinned` already existed, so the rename made five identically named tests in one suite and `--match-test testAbiPinned` select all of them. The file split stands; only the function names go back. Co-Authored-By: Claude Opus 5 (1M context) --- .gas-snapshot | 58 +++++++++---------- .../deprecated/ICloneableFactoryV1.t.sol | 2 +- .../interface/deprecated/ICloneableV1.t.sol | 2 +- test/src/interface/deprecated/IFactory.t.sol | 2 +- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 3b8d5ad..687ae49 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,4 +1,4 @@ -ICloneableFactoryV1DeclarationTest:testAbiPinned() (gas: 10541) +ICloneableFactoryV1DeclarationTest:testICloneableFactoryV1AbiPinned() (gas: 10518) ICloneableFactoryV2DeclarationTest:testAbiPinned() (gas: 10466) ICloneableFactoryV2DeclarationTest:testCloneSelectorPinned() (gas: 290) ICloneableFactoryV2DeclarationTest:testNewCloneTopicRelationshipsAcrossTheFamily() (gas: 270) @@ -7,65 +7,65 @@ ICloneableFactoryV3DeclarationTest:testFunctionAbiPinned() (gas: 15955) ICloneableFactoryV3DeclarationTest:testFunctionSelectorsPinned() (gas: 325) ICloneableFactoryV3DeclarationTest:testNewCloneAbiPinned() (gas: 13998) ICloneableFactoryV3DeclarationTest:testNewCloneNameIsSharedAcrossTheInterfaceFamily() (gas: 313) -ICloneableFactoryV3DeclarationTest:testNewCloneSharedByBothEntryPoints(bytes32,bytes) (runs: 2048, μ: 1002533, ~: 980796) +ICloneableFactoryV3DeclarationTest:testNewCloneSharedByBothEntryPoints(bytes32,bytes) (runs: 2048, μ: 1001856, ~: 980796) ICloneableFactoryV3DeclarationTest:testNewCloneTopicZeroPinned() (gas: 245) -ICloneableV1DeclarationTest:testAbiPinned() (gas: 8133) +ICloneableV1DeclarationTest:testICloneableV1AbiPinned() (gas: 8110) ICloneableV2InitializeTest:testAbiPinned() (gas: 11499) -ICloneableV2InitializeTest:testInitializeOnlyOnce(bytes32,bytes,bytes) (runs: 2048, μ: 436745, ~: 426854) -ICloneableV2InitializeTest:testInitializeRevertBubblesVerbatim(bytes32,bytes) (runs: 2048, μ: 186103, ~: 185874) +ICloneableV2InitializeTest:testInitializeOnlyOnce(bytes32,bytes,bytes) (runs: 2048, μ: 436859, ~: 426854) +ICloneableV2InitializeTest:testInitializeRevertBubblesVerbatim(bytes32,bytes) (runs: 2048, μ: 186098, ~: 185874) ICloneableV2InitializeTest:testInitializeSelectorPinned() (gas: 245) ICloneableV2InitializeTest:testInitializeSelectorSharedWithV1() (gas: 267) ICloneableV2InitializeTest:testInitializeSignatureFnSelectorPinned() (gas: 290) -ICloneableV2InitializeTest:testNothingCalledBeforeInitialize(bytes32,bytes) (runs: 2048, μ: 514886, ~: 503844) +ICloneableV2InitializeTest:testNothingCalledBeforeInitialize(bytes32,bytes) (runs: 2048, μ: 514544, ~: 503844) ICloneableV2InitializeTest:testSuccessSentinelPinned() (gas: 289) -ICloneableV2InitializeTest:testTypedOverloadRevertsInitializeSignatureFn(bytes32,bytes,uint256) (runs: 2048, μ: 431570, ~: 421429) -ICloneableV2InitializeTest:testV1ShapedImplementationIsRejected(bytes32,bytes) (runs: 2048, μ: 352841, ~: 342027) -IFactoryDeclarationTest:testAbiPinned() (gas: 14452) +ICloneableV2InitializeTest:testTypedOverloadRevertsInitializeSignatureFn(bytes32,bytes,uint256) (runs: 2048, μ: 431755, ~: 421429) +ICloneableV2InitializeTest:testV1ShapedImplementationIsRejected(bytes32,bytes) (runs: 2048, μ: 352503, ~: 342027) +IFactoryDeclarationTest:testIFactoryAbiPinned() (gas: 14451) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297955) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEip7702Designator(address,address) (runs: 2048, μ: 7422, ~: 7422) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7963, ~: 7960) LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7169, ~: 7223) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 557083, ~: 537270) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 563920, ~: 544860) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 556515, ~: 537270) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 563780, ~: 544860) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8141, ~: 8126) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 564146, ~: 545415) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 554268, ~: 532640) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 432749, ~: 421907) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 564025, ~: 545415) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 553592, ~: 532640) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 432410, ~: 421907) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6501, ~: 6482) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 551964, ~: 533074) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 435434, ~: 424488) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12249, ~: 12224) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445180, ~: 1040445384) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 553403, ~: 533074) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 435093, ~: 424488) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12248, ~: 12224) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445177, ~: 1040445384) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11113, ~: 11129) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561762, ~: 542710) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 432726, ~: 421889) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561623, ~: 542710) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 432388, ~: 421889) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 551963, ~: 533035) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 434945, ~: 424015) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 553402, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 434604, ~: 424015) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445713, ~: 1040445864) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 558006, ~: 538213) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11089, ~: 11111) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445709, ~: 1040445875) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 557614, ~: 538213) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11088, ~: 11111) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1406, ~: 1398) LibICloneableFactoryV4Test:testDomainTagsDistinct() (gas: 233) LibICloneableFactoryV4Test:testDomainTagsPinned() (gas: 325) -LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4689, ~: 4682) +LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4688, ~: 4682) LibICloneableFactoryV4Test:testEffectiveOpenSaltEmptyData(bytes32) (runs: 2048, μ: 826, ~: 826) LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1270, ~: 1257) LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1361, ~: 1348) -LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4178, ~: 4166) +LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4179, ~: 4166) LibICloneableFactoryV4Test:testEffectiveSaltDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) LibICloneableFactoryV4Test:testEffectiveSaltMatchesFormula(address,bytes32) (runs: 2048, μ: 865, ~: 865) LibICloneableFactoryV4Test:testEffectiveSaltPreimageShape(address,bytes32) (runs: 2048, μ: 966, ~: 966) LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) -TestCloneFactoryTest:testCloneEntryPointsRouteToTheirOwnDerivation(bytes32,bytes) (runs: 2048, μ: 553968, ~: 532270) +TestCloneFactoryTest:testCloneEntryPointsRouteToTheirOwnDerivation(bytes32,bytes) (runs: 2048, μ: 553292, ~: 532270) TestCloneFactoryTest:testImplementsICloneableFactoryV4() (gas: 306) TestCloneFactoryTest:testPredictDeterministicAddressIsPureDelegation(address,bytes32,address) (runs: 2048, μ: 6132, ~: 6132) TestCloneFactoryTest:testPredictDeterministicAddressOpenSaltIsPureDelegation(address,bytes,bytes32) (runs: 2048, μ: 7039, ~: 7020) -TestCloneFactoryTest:testPredictionsAreStatic(address,bytes,bytes32,address) (runs: 2048, μ: 13622, ~: 13590) \ No newline at end of file +TestCloneFactoryTest:testPredictionsAreStatic(address,bytes,bytes32,address) (runs: 2048, μ: 13621, ~: 13590) \ No newline at end of file diff --git a/test/src/interface/deprecated/ICloneableFactoryV1.t.sol b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol index 0184398..3a50ef5 100644 --- a/test/src/interface/deprecated/ICloneableFactoryV1.t.sol +++ b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol @@ -18,7 +18,7 @@ import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; /// redeployed. contract ICloneableFactoryV1DeclarationTest is Test { /// `ICloneableFactoryV1`: the three-parameter `NewClone` and `clone`. - function testAbiPinned() external view { + function testICloneableFactoryV1AbiPinned() external view { assertEq(ICloneableFactoryV1.NewClone.selector, keccak256("NewClone(address,address,address)")); assertEq(ICloneableFactoryV1.clone.selector, bytes4(keccak256("clone(address,bytes)"))); diff --git a/test/src/interface/deprecated/ICloneableV1.t.sol b/test/src/interface/deprecated/ICloneableV1.t.sol index d582c17..d76dd2f 100644 --- a/test/src/interface/deprecated/ICloneableV1.t.sol +++ b/test/src/interface/deprecated/ICloneableV1.t.sol @@ -20,7 +20,7 @@ contract ICloneableV1DeclarationTest is Test { /// `ICloneableV1.initialize` has NO return value. That is the entire /// V1/V2 difference, and it is invisible in the selector, so the ABI is /// the only place it can be pinned. - function testAbiPinned() external view { + function testICloneableV1AbiPinned() external view { assertEq(ICloneableV1.initialize.selector, bytes4(keccak256("initialize(bytes)"))); assertTrue( vm.contains( diff --git a/test/src/interface/deprecated/IFactory.t.sol b/test/src/interface/deprecated/IFactory.t.sol index 0f0cca3..f184d4e 100644 --- a/test/src/interface/deprecated/IFactory.t.sol +++ b/test/src/interface/deprecated/IFactory.t.sol @@ -21,7 +21,7 @@ contract IFactoryDeclarationTest is Test { /// `bool` is load-bearing — the interface calls it CRITICAL to the /// security guarantees of any implementation — and a return type is not /// part of a selector, so only the ABI pins it. - function testAbiPinned() external view { + function testIFactoryAbiPinned() external view { assertEq(IFactory.NewChild.selector, keccak256("NewChild(address,address)")); assertEq(IFactory.Implementation.selector, keccak256("Implementation(address,address)")); assertEq(IFactory.createChild.selector, bytes4(keccak256("createChild(bytes)")));