From 16b136f6943f77d8355a23391e1bb2164ef9567f Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 14:39:36 +0000 Subject: [PATCH 1/7] feat: LibCloneFactory, the ICloneableFactoryV4 logic as a unit-tested library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library half of the split carries the logic, not just the interfaces: both V4 effective-salt derivations, the implementation-code guard, the EIP1167 creation code and CREATE2 prediction constructed from the standard's own bytes, and the atomic clone-initialize-verify flow with typed errors and NewClone. msg.sender and address(this) are read inside the library, so a concrete factory is one delegation per entry point — TestCloneFactory in the suite is exactly that concrete, and the deploy half's CloneFactory is meant to become it. The published src/ imports nothing external; OZ Clones is vendored as a test-only oracle pinning the EIP1167 construction byte for byte, next to the raw CREATE2 formula and the EIP's literal bytes restated in-test. Co-Authored-By: Claude Fable 5 --- .gas-snapshot | 40 ++ foundry.toml | 11 +- soldeer.lock | 7 + src/lib/LibCloneFactory.sol | 225 +++++++++++ test/src/concrete/TestCloneFactory.sol | 51 +++ test/src/concrete/TestCloneable.sol | 19 + test/src/concrete/TestCloneableFailure.sol | 17 + ...CloneFactory.checkImplementationCode.t.sol | 44 +++ .../LibCloneFactory.cloneCreationCode.t.sol | 41 ++ .../LibCloneFactory.cloneDeterministic.t.sol | 179 +++++++++ ...neFactory.cloneDeterministicOpenSalt.t.sol | 374 ++++++++++++++++++ ...CloneFactory.effectiveSaltNamespaced.t.sol | 79 ++++ .../LibCloneFactory.effectiveSaltOpen.t.sol | 47 +++ .../LibCloneFactory.predictCloneAddress.t.sol | 58 +++ 14 files changed, 1188 insertions(+), 4 deletions(-) create mode 100644 .gas-snapshot create mode 100644 src/lib/LibCloneFactory.sol create mode 100644 test/src/concrete/TestCloneFactory.sol create mode 100644 test/src/concrete/TestCloneable.sol create mode 100644 test/src/concrete/TestCloneableFailure.sol create mode 100644 test/src/lib/LibCloneFactory.checkImplementationCode.t.sol create mode 100644 test/src/lib/LibCloneFactory.cloneCreationCode.t.sol create mode 100644 test/src/lib/LibCloneFactory.cloneDeterministic.t.sol create mode 100644 test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol create mode 100644 test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol create mode 100644 test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol create mode 100644 test/src/lib/LibCloneFactory.predictCloneAddress.t.sol diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..9ed1a4e --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,40 @@ +LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) +LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) +LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) +LibCloneFactoryCloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) +LibCloneFactoryCloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 459468, ~: 443058) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 467283, ~: 450603) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 467902, ~: 451203) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 457008, ~: 438428) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 359284, ~: 349969) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6500, ~: 6482) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455790, ~: 438862) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361912, ~: 352503) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040442913, ~: 1040443151) +LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11124, ~: 11129) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 465161, ~: 448453) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 359262, ~: 349951) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455723, ~: 438823) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361424, ~: 352030) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040443446, ~: 1040443589) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 460393, ~: 444001) +LibCloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11087, ~: 11111) +LibCloneFactoryEffectiveSaltNamespacedTest:testDomainTagsDistinct() (gas: 347) +LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) +LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedDisjointFromOpen(address,bytes32,bytes,bytes32) (runs: 2048, μ: 1363, ~: 1356) +LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedIsSpecEquation(address,bytes32) (runs: 2048, μ: 819, ~: 819) +LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedPreimageShape(address,bytes32) (runs: 2048, μ: 933, ~: 933) +LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3753, ~: 3753) +LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenDataSensitive(bytes,bytes,bytes32) (runs: 2048, μ: 4530, ~: 4525) +LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenIsSpecEquation(bytes,bytes32) (runs: 2048, μ: 1237, ~: 1226) +LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenPreimageShape(bytes,bytes32) (runs: 2048, μ: 1243, ~: 1232) +LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenSaltSensitive(bytes,bytes32,bytes32) (runs: 2048, μ: 4092, ~: 4081) +LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) +LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) +LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index f3e327f..ce13e37 100644 --- a/foundry.toml +++ b/foundry.toml @@ -19,12 +19,15 @@ libs = ["dependencies"] [fuzz] runs = 2048 -# Library repo: the ICloneable* interface surface only. The concrete CloneFactory, -# its deploy-pin snapshots and deploy scripts live in rain.factory.deploy -# (rainlanguage/rain.factory#46). Interfaces import nothing, so there are no -# Solidity dependencies beyond the test harness. +# Library repo: the ICloneable* interface surface plus the Lib* logic that +# implements it. The concrete CloneFactory, its deploy-pin snapshots and deploy +# scripts live in rain.factory.deploy (rainlanguage/rain.factory#46). The +# published src/ imports nothing external, so every dependency here is +# test-harness only: forge-std, and OZ Clones as the independent equivalence +# oracle for the EIP1167 construction. [dependencies] forge-std = "1.16.1" +"@openzeppelin-contracts" = "5.6.1" [soldeer] recursive_deps = false diff --git a/soldeer.lock b/soldeer.lock index e9184e4..edfbca8 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -1,3 +1,10 @@ +[[dependencies]] +name = "@openzeppelin-contracts" +version = "5.6.1" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_6_1_15-03-2026_09:19:50_contracts.zip" +checksum = "a3b6bc661be858c7c27f60a1708cbebe8c71034b4cc1e9fe270d0a05b069352f" +integrity = "bce03af7ada1eee21a7fff393f238bcd7cd75a022a4db55ffb6b0dbb32433d35" + [[dependencies]] name = "forge-std" version = "1.16.1" diff --git a/src/lib/LibCloneFactory.sol b/src/lib/LibCloneFactory.sol new file mode 100644 index 0000000..0613662 --- /dev/null +++ b/src/lib/LibCloneFactory.sol @@ -0,0 +1,225 @@ +// 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 "../interface/ICloneableV2.sol"; +import {ICloneableFactoryV3} from "../interface/ICloneableFactoryV3.sol"; +import { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "../interface/ICloneableFactoryV4.sol"; + +/// Thrown when an implementation has zero code size which is always a mistake: +/// an EIP-1167 proxy of a codeless implementation delegates every call — +/// `initialize` included — to nothing. +error ZeroImplementationCodeSize(); + +/// Thrown when the `CREATE2` deploy of the clone itself fails. With the tiny +/// fixed EIP-1167 initcode the only realistic cause is that the effective salt +/// is already taken: the exact clone asked for is already at the address, so +/// the caller can never mistake an already-initialized contract for their own +/// fresh deploy. +error CloneDeploymentFailed(); + +/// Thrown when initialization fails: `ICloneableV2.initialize` on the fresh +/// clone returned something other than `ICLONEABLE_V2_SUCCESS`. +error InitializationFailed(); + +/// @dev The EIP-1167 creation code up to the implementation address: the +/// 10-byte deploy preamble (which returns the trailing 45 bytes as runtime +/// code) followed by the first 10 bytes of the runtime. +bytes constant EIP1167_CREATION_CODE_PREFIX = hex"3d602d80600a3d3981f3363d3d373d3d3d363d73"; + +/// @dev The EIP-1167 bytes after the implementation address, shared by the +/// creation code and the runtime code. +bytes constant EIP1167_CREATION_CODE_SUFFIX = hex"5af43d82803e903d91602b57fd5bf3"; + +/// @title LibCloneFactory +/// @notice The whole of an `ICloneableFactoryV4` factory as internal library +/// logic, so a concrete factory is nothing but one delegation per entry point. +/// This is the library half of the library/deploy split +/// (rainlanguage/rain.factory#46): the derivations, guards and the +/// clone-initialize-verify flow live here, unit tested; the deploy half's +/// concrete `CloneFactory` adds no behaviour of its own. +/// +/// Both effective-salt derivations are the ones `ICloneableFactoryV4` pins to +/// exact bytes — a `keccak256` over a 96-byte preimage whose FIRST word is a +/// distinct, string-derived domain tag the caller cannot set — and its NatSpec +/// is the spec for everything here: the disjointness of the two images, the +/// atomic clone-and-initialize, and the `NewClone` event carrying the RAW +/// caller salt. +/// +/// `msg.sender` is read INSIDE this library — `cloneDeterministic` namespaces +/// by it and `NewClone` reports it — and the internal functions execute in the +/// factory's own call context, so a delegating concrete cannot get either +/// wrong: there is no sender parameter to misroute `tx.origin` into. Likewise +/// the predictions read `address(this)`, the factory the library is inlined +/// into. +library LibCloneFactory { + /// The effective `CREATE2` salt of the namespaced pair, exactly as + /// `ICloneableFactoryV4` fixes it: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt))`. + /// The address this salt produces commits to WHO deployed — nobody but + /// `deployer` can reach it through the factory — and not to what was + /// deployed there. + /// @param deployer The account whose deploy this is: `msg.sender` when + /// deploying, the `deployer` argument when predicting. + /// @param salt The caller-chosen salt. + /// @return The effective `CREATE2` salt. + function effectiveSaltNamespaced(address deployer, bytes32 salt) internal pure returns (bytes32) { + return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)); + } + + /// The effective `CREATE2` salt of the open-salt pair, exactly as + /// `ICloneableFactoryV4` fixes it: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. + /// Nothing caller-derived is hashed in, so every account lands on the same + /// address — and because `data` is hashed in, every account that lands + /// there deploys the same contract initialized with the same bytes. + /// @param data The initialization data, part of the address derivation. + /// @param salt The caller-chosen salt. + /// @return The effective `CREATE2` salt. + function effectiveSaltOpen(bytes memory data, bytes32 salt) internal pure returns (bytes32) { + return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); + } + + /// The canonical 55-byte EIP-1167 creation code for `implementation`. + /// Constructed from the standard's bytes directly so this library — and + /// with it the published factory — depends on no external cloning code; + /// the tests pin it byte for byte against OZ `Clones` as a foreign + /// implementation of the same standard. + /// @param implementation The contract the deployed proxy will delegate to. + /// @return The creation code. + function cloneCreationCode(address implementation) internal pure returns (bytes memory) { + return abi.encodePacked(EIP1167_CREATION_CODE_PREFIX, implementation, EIP1167_CREATION_CODE_SUFFIX); + } + + /// The address `CREATE2` gives the EIP-1167 clone of `implementation` from + /// `factory` at `effectiveSalt`: + /// `address(keccak256(0xff ++ factory ++ effectiveSalt ++ keccak256(creationCode)))`. + /// A pure function of its inputs, computable by anyone offchain. + /// @param factory The factory that would deploy the clone. + /// @param implementation The contract to clone. + /// @param effectiveSalt The effective `CREATE2` salt, from one of the two + /// derivations above. + /// @return The predicted clone address. + function predictCloneAddress(address factory, address implementation, bytes32 effectiveSalt) + internal + pure + returns (address) + { + return address( + uint160( + uint256( + keccak256( + abi.encodePacked(hex"ff", factory, effectiveSalt, keccak256(cloneCreationCode(implementation))) + ) + ) + ) + ); + } + + /// Reverts with `ZeroImplementationCodeSize` if `implementation` has no + /// code. Always a mistake: the clone would delegate every call to nothing. + /// @param implementation The contract to clone. + function checkImplementationCode(address implementation) internal view { + if (implementation.code.length == 0) { + revert ZeroImplementationCodeSize(); + } + } + + /// The shared tail of both clone entry points: guard the implementation, + /// `CREATE2` the EIP-1167 clone at `effectiveSalt`, emit `NewClone` with + /// the RAW caller salt, then run the mandatory `ICloneableV2.initialize` + /// check — atomically, with nothing else called on the proxy first, and + /// the clone only considered created if `initialize` returns + /// `ICLONEABLE_V2_SUCCESS`, per the shared spec on + /// `ICloneableFactoryV3.cloneDeterministic`. + /// @param implementation The contract to clone. + /// @param effectiveSalt The effective `CREATE2` salt, from one of the two + /// derivations above. + /// @param data The initialization data, forwarded verbatim to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt, emitted raw in `NewClone`. + /// @return The deployed and initialized child contract address. + function cloneAndInitialize(address implementation, bytes32 effectiveSalt, bytes memory data, bytes32 salt) + internal + returns (address) + { + checkImplementationCode(implementation); + bytes memory creationCode = cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), effectiveSalt) + } + if (child == address(0)) { + revert CloneDeploymentFailed(); + } + emit ICloneableFactoryV3.NewClone(msg.sender, implementation, child, salt, data); + // Checking the return value of initialize is mandatory as per + // ICloneableFactoryV3 and ICloneableFactoryV4. + if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) { + revert InitializationFailed(); + } + return child; + } + + /// `ICloneableFactoryV3.cloneDeterministic`, whole: the namespaced + /// derivation over `msg.sender` — read here, not passed, so a delegating + /// concrete cannot namespace by anything else — then the shared + /// clone-initialize-verify flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministic(address implementation, bytes memory data, bytes32 salt) internal returns (address) { + return cloneAndInitialize(implementation, effectiveSaltNamespaced(msg.sender, salt), data, salt); + } + + /// `ICloneableFactoryV3.predictDeterministicAddress`, whole: the address + /// `cloneDeterministic(implementation, _, salt)` deploys to when called by + /// `deployer` on the factory this library is inlined into. + /// @param implementation The contract to clone. + /// @param salt The caller-chosen salt. + /// @param deployer The account that will call `cloneDeterministic`. + /// @return The predicted clone address. + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveSaltNamespaced(deployer, salt)); + } + + /// `ICloneableFactoryV4.cloneDeterministicOpenSalt`, whole: the open-salt + /// derivation — no caller-derived value hashed in — then the shared + /// clone-initialize-verify flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`, and part of the address derivation. + /// MAY be empty. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministicOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + returns (address) + { + return cloneAndInitialize(implementation, effectiveSaltOpen(data, salt), data, salt); + } + + /// `ICloneableFactoryV4.predictDeterministicAddressOpenSalt`, whole: the + /// address `cloneDeterministicOpenSalt(implementation, data, salt)` + /// deploys to from the factory this library is inlined into, whoever calls + /// it. + /// @param implementation The contract to clone. + /// @param data The initialization data that will be passed to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt. + /// @return The predicted clone address. + function predictDeterministicAddressOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveSaltOpen(data, salt)); + } +} diff --git a/test/src/concrete/TestCloneFactory.sol b/test/src/concrete/TestCloneFactory.sol new file mode 100644 index 0000000..f3bc375 --- /dev/null +++ b/test/src/concrete/TestCloneFactory.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +// `ICloneableFactoryV3` is imported for the `@inheritdoc` references on the +// functions it declares; `ICloneableFactoryV4` inherits rather than redeclares +// them, so the tag must name V3 and V3 must be in scope here. +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol"; +import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; + +/// @title TestCloneFactory +/// @notice A concrete `ICloneableFactoryV4` written the way the deploy half is +/// meant to write one: every function is a single delegation into +/// `LibCloneFactory` and nothing else. It exists so the flow tests exercise the +/// library through a real external surface — `msg.sender` namespacing and the +/// `NewClone` event are observable only across an external call — and it +/// doubles as the executable proof that the library surface suffices for a +/// pure-delegation concrete. +contract TestCloneFactory is ICloneableFactoryV4 { + /// @inheritdoc ICloneableFactoryV3 + function cloneDeterministic(address implementation, bytes calldata data, bytes32 salt) external returns (address) { + return LibCloneFactory.cloneDeterministic(implementation, data, salt); + } + + /// @inheritdoc ICloneableFactoryV3 + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + external + view + returns (address) + { + return LibCloneFactory.predictDeterministicAddress(implementation, salt, deployer); + } + + /// @inheritdoc ICloneableFactoryV4 + function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + returns (address) + { + return LibCloneFactory.cloneDeterministicOpenSalt(implementation, data, salt); + } + + /// @inheritdoc ICloneableFactoryV4 + function predictDeterministicAddressOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + view + returns (address) + { + return LibCloneFactory.predictDeterministicAddressOpenSalt(implementation, data, salt); + } +} diff --git a/test/src/concrete/TestCloneable.sol b/test/src/concrete/TestCloneable.sol new file mode 100644 index 0000000..6fcf0a7 --- /dev/null +++ b/test/src/concrete/TestCloneable.sol @@ -0,0 +1,19 @@ +// 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 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. +contract TestCloneable is ICloneableV2 { + bytes public sData; + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + sData = data; + return ICLONEABLE_V2_SUCCESS; + } +} diff --git a/test/src/concrete/TestCloneableFailure.sol b/test/src/concrete/TestCloneableFailure.sol new file mode 100644 index 0000000..5088ebb --- /dev/null +++ b/test/src/concrete/TestCloneableFailure.sol @@ -0,0 +1,17 @@ +// 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 TestCloneableFailure +/// @notice A cloneable contract that implements `ICloneableV2` but always +/// fails initialization. Specifically, it returns whatever data is passed to +/// `initialize`, which is expected NOT to be `ICLONEABLE_V2_SUCCESS` for the +/// purposes of testing. +contract TestCloneableFailure is ICloneableV2 { + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external pure returns (bytes32) { + return abi.decode(data, (bytes32)); + } +} diff --git a/test/src/lib/LibCloneFactory.checkImplementationCode.t.sol b/test/src/lib/LibCloneFactory.checkImplementationCode.t.sol new file mode 100644 index 0000000..8618697 --- /dev/null +++ b/test/src/lib/LibCloneFactory.checkImplementationCode.t.sol @@ -0,0 +1,44 @@ +// 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 {LibCloneFactory, ZeroImplementationCodeSize} from "src/lib/LibCloneFactory.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; + +/// @title LibCloneFactoryCheckImplementationCodeTest +/// @notice Tests `LibCloneFactory.checkImplementationCode`: a codeless +/// implementation is always a mistake — its clone would delegate every call, +/// `initialize` included, to nothing — so it reverts with a typed error +/// before any deploy happens. +contract LibCloneFactoryCheckImplementationCodeTest is Test { + /// External wrapper so `vm.expectRevert` sees the internal library call at + /// its own call depth. + function checkImplementationCodeExternal(address implementation) external view { + LibCloneFactory.checkImplementationCode(implementation); + } + + /// Any address without code reverts `ZeroImplementationCodeSize`. + function testCheckImplementationCodeZero(address implementation) external { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + this.checkImplementationCodeExternal(implementation); + } + + /// A deployed contract passes. + function testCheckImplementationCodeContract() external { + TestCloneable implementation = new TestCloneable(); + LibCloneFactory.checkImplementationCode(address(implementation)); + } + + /// Any nonempty code is enough to pass: the guard is a code-size check, + /// not a validation of what the code is. + function testCheckImplementationCodeEtched(address implementation, bytes memory code) external { + vm.assume(implementation.code.length == 0); + vm.assume(uint160(implementation) > 0x0a); + vm.assume(code.length > 0); + vm.etch(implementation, code); + LibCloneFactory.checkImplementationCode(implementation); + } +} diff --git a/test/src/lib/LibCloneFactory.cloneCreationCode.t.sol b/test/src/lib/LibCloneFactory.cloneCreationCode.t.sol new file mode 100644 index 0000000..e514730 --- /dev/null +++ b/test/src/lib/LibCloneFactory.cloneCreationCode.t.sol @@ -0,0 +1,41 @@ +// 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 {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; + +/// @title LibCloneFactoryCloneCreationCodeTest +/// @notice Tests `LibCloneFactory.cloneCreationCode` against the EIP-1167 +/// bytes written out literally here, from the EIP, so the library's constants +/// are pinned against the standard rather than against themselves. +contract LibCloneFactoryCloneCreationCodeTest is Test { + /// The creation code is the canonical 55-byte EIP-1167 sequence: the + /// 10-byte deploy preamble, the 10-byte runtime prefix, the 20-byte + /// implementation address and the 15-byte runtime suffix. + function testCloneCreationCodeIsEIP1167(address implementation) external pure { + bytes memory expected = abi.encodePacked( + hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" + ); + bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + assertEq(creationCode.length, 55); + assertEq(creationCode, expected); + } + + /// Deploying the creation code really produces the EIP-1167 RUNTIME code + /// for the implementation: the same bytes minus the 10-byte deploy + /// preamble. This pins the preamble's semantics (codecopy of the trailing + /// 45 bytes) and not just its bytes. + function testCloneCreationCodeDeploysEIP1167Runtime(address implementation, bytes32 salt) external { + bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), salt) + } + assertTrue(child != address(0)); + bytes memory expectedRuntime = + abi.encodePacked(hex"363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3"); + assertEq(child.code, expectedRuntime); + } +} diff --git a/test/src/lib/LibCloneFactory.cloneDeterministic.t.sol b/test/src/lib/LibCloneFactory.cloneDeterministic.t.sol new file mode 100644 index 0000000..72a45e0 --- /dev/null +++ b/test/src/lib/LibCloneFactory.cloneDeterministic.t.sol @@ -0,0 +1,179 @@ +// 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 {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; +import {CloneDeploymentFailed, InitializationFailed, ZeroImplementationCodeSize} from "src/lib/LibCloneFactory.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"; + +/// @title LibCloneFactoryCloneDeterministicTest +/// @notice Tests `LibCloneFactory.cloneDeterministic` / +/// `predictDeterministicAddress` — the namespaced pair — through +/// `TestCloneFactory`, a pure-delegation concrete, because `msg.sender` +/// namespacing and the `NewClone` event only exist across an external call. +/// The defining property is that the address commits to WHO deployed — +/// `(deployer, salt)` — and not to WHAT was initialized. +contract LibCloneFactoryCloneDeterministicTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The effective `CREATE2` salt is exactly the derivation + /// `ICloneableFactoryV4` fixes: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))`. + /// Pinned against OZ's own prediction under an independently constructed + /// salt, so an off-chain caller can reproduce the address from + /// `(implementation, deployer, salt, factory)` alone and the test does not + /// restate the library's arithmetic back to itself. + function testCloneDeterministicSaltIsDomainTaggedHash(address implementation, bytes32 salt, address deployer) + external + view + { + bytes32 effectiveSalt = keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)); + address expected = Clones.predictDeterministicAddress(implementation, effectiveSalt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer), expected); + } + + /// The deployed clone lands at the predicted address, is an EIP-1167 proxy + /// of the implementation — its runtime code compared against the EIP's + /// bytes written out literally — and is initialized with the data. + /// `predict` therefore lets a caller pin the address before deploying. + function testCloneDeterministicMatchesPredict(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(child, predicted); + assertEq( + child.code, + abi.encodePacked(hex"363d3d373d3d3d363d73", address(implementation), hex"5af43d82803e903d91602b57fd5bf3") + ); + assertEq(TestCloneable(child).sData(), data); + } + + /// Distinct salts yield distinct clones of the same implementation — many + /// clones per impl (unlike a salt-free / one-per-impl deterministic + /// deploy). + function testCloneDeterministicManyClonesPerImpl(bytes32 salt1, bytes32 salt2, bytes memory data) external { + vm.assume(salt1 != salt2); + TestCloneable implementation = new TestCloneable(); + + address child1 = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt1); + address child2 = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt2); + assertTrue(child1 != child2); + } + + /// The same `(implementation, salt)` from different callers yields + /// different addresses: the salt is namespaced by `msg.sender`, so no + /// caller can squat or front-run another's address. `predict` reflects the + /// deployer. + function testCloneDeterministicSenderScoped(bytes32 salt, bytes memory data, address alice, address bob) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + address predictedAlice = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, alice); + address predictedBob = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, bob); + assertTrue(predictedAlice != predictedBob); + + vm.prank(alice); + address childAlice = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(childAlice, predictedAlice); + + vm.prank(bob); + address childBob = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(childBob, predictedBob); + + assertTrue(childAlice != childBob); + } + + /// `data` is NOT in the derivation: the same caller and salt with two + /// different `data` land on the SAME address, each initialized with its + /// own bytes. State is snapshotted and rolled back between the two deploys + /// so both genuinely happen from the same starting state. This is the + /// namespaced pair's trade documented on `ICloneableFactoryV4`: the + /// deployer alone decides the clone's initial state at an address that + /// says nothing about it. + function testCloneDeterministicDataNotInDerivation(bytes32 salt, bytes memory dataA, bytes memory dataB) external { + vm.assume(keccak256(dataA) != keccak256(dataB)); + TestCloneable implementation = new TestCloneable(); + + uint256 snapshot = vm.snapshotState(); + + address childA = I_CLONE_FACTORY.cloneDeterministic(address(implementation), dataA, salt); + bytes memory sDataA = TestCloneable(childA).sData(); + + vm.revertToState(snapshot); + + address childB = I_CLONE_FACTORY.cloneDeterministic(address(implementation), dataB, salt); + + assertEq(childA, childB); + assertEq(sDataA, dataA); + assertEq(TestCloneable(childB).sData(), dataB); + } + + /// A second deploy at an already-taken `(deployer, salt)` reverts with the + /// library's own typed error: a caller can never mistake an + /// already-initialized contract for their own fresh deploy. + function testCloneDeterministicSecondDeployReverts(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(CloneDeploymentFailed.selector)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + // The first deploy's state is untouched by the failed second one. + assertEq(TestCloneable(child).sData(), data); + } + + /// `NewClone` is emitted with the caller, implementation, child, salt and + /// data — the full deterministic deploy, reconstructable from the event + /// alone. The salt is the RAW caller salt, not the effective one. + function testCloneDeterministicEvent(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)")))); + assertEq(entries[0].data, abi.encode(address(this), address(implementation), child, salt, data)); + } + + /// An implementation that initializes to a non-success code reverts + /// `InitializationFailed`, so clone-and-initialize stays atomic and the + /// address is left free rather than occupied by an uninitialized clone. + function testCloneDeterministicInitializeFailureFails(bytes32 notSuccess, bytes32 salt) external { + vm.assume(notSuccess != ICLONEABLE_V2_SUCCESS); + TestCloneableFailure implementation = new TestCloneableFailure(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), abi.encode(notSuccess), salt); + + assertEq(predicted.code.length, 0); + } + + /// A zero-code implementation reverts `ZeroImplementationCodeSize`. + function testCloneDeterministicZeroImplementationCodeSize(address implementation, bytes memory data, bytes32 salt) + external + { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + I_CLONE_FACTORY.cloneDeterministic(implementation, data, salt); + } +} diff --git a/test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol new file mode 100644 index 0000000..f17a96b --- /dev/null +++ b/test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol @@ -0,0 +1,374 @@ +// 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 {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "src/interface/ICloneableFactoryV4.sol"; +import { + LibCloneFactory, + CloneDeploymentFailed, + InitializationFailed, + ZeroImplementationCodeSize +} from "src/lib/LibCloneFactory.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"; + +/// @title LibCloneFactoryCloneDeterministicOpenSaltTest +/// @notice Tests `LibCloneFactory.cloneDeterministicOpenSalt` / +/// `predictDeterministicAddressOpenSalt` through `TestCloneFactory`, a +/// pure-delegation concrete. The defining property is that the address commits +/// to WHAT is deployed — `(implementation, data, salt)` — and to nothing about +/// WHO deploys it, which is the exact opposite of what `cloneDeterministic` +/// guarantees. So the two derivations are also tested against each other here, +/// including the one squat that the pair of distinct domain tags exists to +/// close. +contract LibCloneFactoryCloneDeterministicOpenSaltTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The effective `CREATE2` salt is exactly the derivation + /// `ICloneableFactoryV4` fixes: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. + /// Pinned against OZ's own prediction under an independently constructed + /// salt, so an off-chain caller can reproduce the address from + /// `(implementation, data, salt, factory)` alone and the test does not + /// restate the library's arithmetic back to itself. + function testCloneDeterministicOpenSaltIsDomainTaggedHash(address implementation, bytes memory data, bytes32 salt) + external + view + { + bytes32 effectiveSalt = keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); + address expected = Clones.predictDeterministicAddress(implementation, effectiveSalt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt), expected); + } + + /// The deployed clone lands at the predicted address, is an EIP-1167 proxy + /// of the implementation — its runtime code compared against the EIP's + /// bytes written out literally — and is initialized with the data. + /// `predict` therefore lets a caller pin the address before deploying — + /// but only once `data` is final, since `data` is in the derivation. + function testCloneDeterministicOpenSaltMatchesPredict(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(child, predicted); + assertEq( + child.code, + abi.encodePacked(hex"363d3d373d3d3d363d73", address(implementation), hex"5af43d82803e903d91602b57fd5bf3") + ); + assertEq(TestCloneable(child).sData(), data); + } + + /// THE POINT OF THIS VARIANT. The same `(implementation, data, salt)` from + /// two different callers lands on the SAME address. State is snapshotted + /// and rolled back between the two deploys so both callers genuinely + /// deploy from the same starting state — the addresses are compared, not + /// merely predicted. This is exactly what `cloneDeterministic` forbids, so + /// an address deployed here survives its original deployer being retired: + /// any other account can re-establish it on another chain. + function testCloneDeterministicOpenSaltCallerIndependent( + bytes32 salt, + bytes memory data, + address alice, + address bob + ) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + + uint256 snapshot = vm.snapshotState(); + + vm.prank(alice); + address childAlice = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + vm.revertToState(snapshot); + + vm.prank(bob); + address childBob = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(childAlice, childBob); + assertEq(childAlice, predicted); + } + + /// The prediction takes no deployer, so it cannot vary with one. + /// Predicting the same `(implementation, data, salt)` from two different + /// callers returns the same address — a caller pinning an address offchain + /// does not need to know who will deploy it. + function testCloneDeterministicOpenSaltPredictCallerIndependent( + address implementation, + bytes memory data, + bytes32 salt, + address alice, + address bob + ) external { + vm.assume(alice != bob); + + vm.prank(alice); + address predictedAlice = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt); + + vm.prank(bob); + address predictedBob = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt); + + assertEq(predictedAlice, predictedBob); + } + + /// `data` IS IN THE DERIVATION, which is what makes losing the + /// `msg.sender` namespacing safe. Two different `data` at the SAME + /// `(implementation, salt)` are two different addresses, and both clones + /// exist independently with their own initialization. So a front-runner + /// who passes anything other than the intended bytes deploys their own + /// contract at their own address and at their own expense, leaving the + /// address that was pinned untouched and still deployable. + function testCloneDeterministicOpenSaltDataInDerivation(bytes32 salt, bytes memory dataA, bytes memory dataB) + external + { + vm.assume(keccak256(dataA) != keccak256(dataB)); + TestCloneable implementation = new TestCloneable(); + + address predictedA = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), dataA, salt); + address predictedB = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), dataB, salt); + assertTrue(predictedA != predictedB); + + address childA = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataA, salt); + address childB = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataB, salt); + + assertEq(childA, predictedA); + assertEq(childB, predictedB); + assertEq(TestCloneable(childA).sData(), dataA); + assertEq(TestCloneable(childB).sData(), dataB); + } + + /// The two derivations are disjoint under freely varying inputs on BOTH + /// sides: no `(data, openSalt)` open-salt address is any `(namespacedSalt, + /// deployer)` sender-namespaced address. Adding the open variant therefore + /// cannot reach, block or collide with an address that `cloneDeterministic` + /// promised to a specific caller, or vice versa. + /// + /// This is the broad statement, and on its own it is weak: a collision it + /// could catch needs a keccak256 collision, so no realistic mutation of + /// the derivation makes it fail. It is kept as the plain form of the + /// interface's claim. The reachable case that actually discriminates the + /// two domain tags is the next test. + function testCloneDeterministicOpenSaltDiffersFromSenderNamespaced( + address implementation, + bytes memory data, + bytes32 openSalt, + bytes32 namespacedSalt, + address deployer + ) external view { + address open = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, openSalt); + address namespaced = I_CLONE_FACTORY.predictDeterministicAddress(implementation, namespacedSalt, deployer); + assertTrue(open != namespaced); + } + + /// THE SQUAT THE DISTINCT DOMAIN TAGS CLOSE, stated as an attack rather + /// than as an absence. + /// + /// `ICloneableFactoryV4` makes it a MUST NOT on the factory that no other + /// entry point can `CREATE2` in the open-salt derivation's image with + /// caller-supplied `data`. `cloneDeterministic` is precisely such an entry + /// point. Both preimages are 96 bytes and the caller chooses words 1 and 2 + /// of each: on the namespaced path `(msg.sender, salt)` and on the open + /// path `(salt, keccak256(data))`. Were the two tags ONE WORD — the same + /// tag on both derivations — an account `A` would reach EVERY open-salt + /// address whose `salt` equals `bytes32(uint256(uint160(A)))`, the + /// abi-encoding of `A`, by calling + /// `cloneDeterministic(implementation, evilData, keccak256(data))`. No + /// preimage search, just a choice of salt. Only the first word — the tag + /// no caller can set — separates the two preimages at that point. + /// + /// The premise is proven, not narrated: the attacker's namespaced words 1 + /// and 2, re-hashed under the OPEN tag, land byte for byte on the honest + /// party's open-salt effective salt. Then the guarantee: under the two + /// DISTINCT tags the real predictions differ, the attacker's deploy lands + /// at their own namespaced address, and the pinned open-salt address is + /// still free and still deploys the intended clone with the intended + /// bytes. + function testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat( + address attacker, + bytes memory data, + bytes memory evilData + ) external { + TestCloneable implementation = new TestCloneable(); + + // The open salt an honest party pinned, which happens to be the + // abi-encoding of the attacker's own address. Nothing stops a salt + // taking this value; the attacker is free to go looking for one that + // does, or to pick the address to suit the salt. + bytes32 openSalt = bytes32(uint256(uint160(attacker))); + address open = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, openSalt); + + // The attacker's namespaced salt is just `keccak256(data)`, read off + // the honest deploy they are front-running. From `attacker` the + // namespaced preimage is then + // `(NAMESPACED_DOMAIN, attacker, keccak256(data))`. + bytes32 attackerSalt = keccak256(data); + address namespaced = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), attackerSalt, attacker); + + // ATTACK PREMISE. `abi.encode` left-pads `attacker` into exactly the + // word `openSalt` already is, so the attacker's namespaced preimage + // re-tagged with the OPEN tag is byte for byte the honest open + // preimage: with one shared tag the squat lands exactly on the address + // the honest party pinned. + bytes32 sharedTagCounterfactual = + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, attacker, attackerSalt)); + assertEq( + sharedTagCounterfactual, + LibCloneFactory.effectiveSaltOpen(data, openSalt), + "the attacker's words re-tagged ARE the open effective salt" + ); + + // And symmetrically under the NAMESPACED tag: the honest open words + // re-tagged are byte for byte the attacker's namespaced effective + // salt. Same two unknowns, solved from the other side, closed by the + // same word. + bytes32 sharedTagCounterfactualNamespaced = + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, openSalt, keccak256(data))); + assertEq( + sharedTagCounterfactualNamespaced, + LibCloneFactory.effectiveSaltNamespaced(attacker, attackerSalt), + "the open words re-tagged ARE the namespaced effective salt" + ); + + // THE GUARANTEE. The two distinct tags move the real open-salt address + // off the one `cloneDeterministic` can reach. + assertTrue(open != namespaced); + + // End to end, not just in prediction: the attacker really deploys, at + // their own address, with their own data, and the honest open-salt + // address is still free afterwards and still deploys the intended + // clone with the intended bytes. + vm.prank(attacker); + address childAttacker = I_CLONE_FACTORY.cloneDeterministic(address(implementation), evilData, attackerSalt); + assertEq(childAttacker, namespaced); + assertEq(open.code.length, 0); + + address childOpen = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, openSalt); + assertEq(childOpen, open); + assertEq(TestCloneable(childOpen).sData(), data); + } + + /// REGRESSION GUARD on the guarantee that must not break, in the other + /// direction. Taking a salt via the open variant does not consume it for + /// `cloneDeterministic`: the same caller can still deploy at the same + /// `salt` through the namespaced derivation, at the address it always + /// predicted, and both clones exist independently. + function testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predictedNamespaced = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + address childOpen = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + address childNamespaced = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(childNamespaced, predictedNamespaced); + assertTrue(childOpen != childNamespaced); + assertTrue(childOpen.code.length > 0); + assertTrue(childNamespaced.code.length > 0); + } + + /// Distinct salts yield distinct clones of the same implementation and the + /// same `data` — many clones per impl, as with the namespaced variant. The + /// other half of "distinct `(salt, data)` pairs yield distinct clones"; + /// the `data` half is `…DataInDerivation`. + function testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32 salt1, bytes32 salt2, bytes memory data) external { + vm.assume(salt1 != salt2); + TestCloneable implementation = new TestCloneable(); + + address child1 = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt1); + address child2 = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt2); + assertTrue(child1 != child2); + } + + /// A second deploy at an already-taken open salt REVERTS with the + /// library's own typed error. Since `data` is in the derivation, repeating + /// the whole `(implementation, data, salt)` is now the ONLY way to aim at + /// an address somebody else already took, and even that does not silently + /// return the existing clone: a caller can never mistake an + /// already-initialized contract for their own fresh deploy. What the + /// reverting caller would have deployed is byte-identical to what is + /// already there, so the loss is the gas and nothing else. + function testCloneDeterministicOpenSaltSecondDeployReverts( + bytes32 salt, + bytes memory data, + address alice, + address bob + ) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + vm.prank(alice); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(CloneDeploymentFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + // The first deploy's state is untouched by the failed second one. + assertEq(TestCloneable(child).sData(), data); + } + + /// `NewClone` is emitted with the caller, implementation, child, salt and + /// data. The event is shared with `cloneDeterministic` and carries the RAW + /// salt in both cases, never the effective one. `salt` and `data` together + /// are the whole of the open derivation, so the event carries enough to + /// recompute the address — an indexer picks the derivation by trying both + /// and keeping the match, which is well defined precisely because the two + /// images are disjoint. + function testCloneDeterministicOpenSaltEvent(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)")))); + assertEq(entries[0].data, abi.encode(address(this), address(implementation), child, salt, data)); + } + + /// An implementation that initializes to a non-success code reverts + /// `InitializationFailed`, so clone-and-initialize stays atomic and the + /// address is left free rather than occupied by an uninitialized clone. + function testCloneDeterministicOpenSaltInitializeFailureFails(bytes32 notSuccess, bytes32 salt) external { + vm.assume(notSuccess != ICLONEABLE_V2_SUCCESS); + TestCloneableFailure implementation = new TestCloneableFailure(); + + bytes memory data = abi.encode(notSuccess); + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } + + /// A zero-code implementation reverts `ZeroImplementationCodeSize`. + function testCloneDeterministicOpenSaltZeroImplementationCodeSize( + address implementation, + bytes memory data, + bytes32 salt + ) external { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(implementation, data, salt); + } +} diff --git a/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol b/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol new file mode 100644 index 0000000..07ef004 --- /dev/null +++ b/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol @@ -0,0 +1,79 @@ +// 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 { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "src/interface/ICloneableFactoryV4.sol"; +import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; + +/// @title LibCloneFactoryEffectiveSaltNamespacedTest +/// @notice Tests `LibCloneFactory.effectiveSaltNamespaced` against the +/// derivation `ICloneableFactoryV4` pins to exact bytes: +/// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))`. +/// The expected values are restated from the interface NatSpec, not read back +/// from the library, so any drift in the library's arithmetic fails here. +contract LibCloneFactoryEffectiveSaltNamespacedTest is Test { + /// The derivation is exactly the spec equation. + function testEffectiveSaltNamespacedIsSpecEquation(address deployer, bytes32 salt) external pure { + assertEq( + LibCloneFactory.effectiveSaltNamespaced(deployer, salt), + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)) + ); + } + + /// The preimage is 96 bytes whose FIRST word is the string-derived + /// namespaced tag. Packed independently of both the library and the + /// interface constant, so this also pins the constant to its documented + /// string. + function testEffectiveSaltNamespacedPreimageShape(address deployer, bytes32 salt) external pure { + bytes memory preimage = + abi.encodePacked(keccak256("rain.factory.clone.namespaced"), bytes32(uint256(uint160(deployer))), salt); + assertEq(preimage.length, 96); + assertEq(LibCloneFactory.effectiveSaltNamespaced(deployer, salt), keccak256(preimage)); + } + + /// The deployer is in the derivation: two deployers, two salts. + function testEffectiveSaltNamespacedDeployerSensitive(address alice, address bob, bytes32 salt) external pure { + vm.assume(alice != bob); + assertTrue( + LibCloneFactory.effectiveSaltNamespaced(alice, salt) != LibCloneFactory.effectiveSaltNamespaced(bob, salt) + ); + } + + /// The caller salt is in the derivation: two salts, two effective salts. + function testEffectiveSaltNamespacedSaltSensitive(address deployer, bytes32 saltA, bytes32 saltB) external pure { + vm.assume(saltA != saltB); + assertTrue( + LibCloneFactory.effectiveSaltNamespaced(deployer, saltA) + != LibCloneFactory.effectiveSaltNamespaced(deployer, saltB) + ); + } + + /// The two domain tags are distinct words, each pinned to its documented + /// string. This is the anchor of the image disjointness: everything else + /// about the two preimages is caller-chosen, the first word is not. + function testDomainTagsDistinct() external pure { + assertEq(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, keccak256("rain.factory.clone.namespaced")); + assertEq(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, keccak256("rain.factory.clone.opensalt")); + assertTrue(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN != ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN); + } + + /// The namespaced image is disjoint from the open-salt image under freely + /// varying inputs on both sides: the first preimage word differs by + /// construction, so equality of the hashes would be a keccak256 collision. + function testEffectiveSaltNamespacedDisjointFromOpen( + address deployer, + bytes32 namespacedSalt, + bytes memory data, + bytes32 openSalt + ) external pure { + assertTrue( + LibCloneFactory.effectiveSaltNamespaced(deployer, namespacedSalt) + != LibCloneFactory.effectiveSaltOpen(data, openSalt) + ); + } +} diff --git a/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol b/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol new file mode 100644 index 0000000..550f4bf --- /dev/null +++ b/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol @@ -0,0 +1,47 @@ +// 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 {ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; +import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; + +/// @title LibCloneFactoryEffectiveSaltOpenTest +/// @notice Tests `LibCloneFactory.effectiveSaltOpen` against the derivation +/// `ICloneableFactoryV4` pins to exact bytes: +/// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. +/// The expected values are restated from the interface NatSpec, not read back +/// from the library. +contract LibCloneFactoryEffectiveSaltOpenTest is Test { + /// The derivation is exactly the spec equation. + function testEffectiveSaltOpenIsSpecEquation(bytes memory data, bytes32 salt) external pure { + assertEq( + LibCloneFactory.effectiveSaltOpen(data, salt), + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))) + ); + } + + /// The preimage is 96 bytes whose FIRST word is the string-derived + /// open-salt tag, whose second is the caller salt and whose third is + /// `keccak256(data)` — `data` enters by hash, so the preimage is fixed + /// length for any data length. Packed independently of both the library + /// and the interface constant. + function testEffectiveSaltOpenPreimageShape(bytes memory data, bytes32 salt) external pure { + bytes memory preimage = abi.encodePacked(keccak256("rain.factory.clone.opensalt"), salt, keccak256(data)); + assertEq(preimage.length, 96); + assertEq(LibCloneFactory.effectiveSaltOpen(data, salt), keccak256(preimage)); + } + + /// `data` is in the derivation: two data, two effective salts. + function testEffectiveSaltOpenDataSensitive(bytes memory dataA, bytes memory dataB, bytes32 salt) external pure { + vm.assume(keccak256(dataA) != keccak256(dataB)); + assertTrue(LibCloneFactory.effectiveSaltOpen(dataA, salt) != LibCloneFactory.effectiveSaltOpen(dataB, salt)); + } + + /// The caller salt is in the derivation: two salts, two effective salts. + function testEffectiveSaltOpenSaltSensitive(bytes memory data, bytes32 saltA, bytes32 saltB) external pure { + vm.assume(saltA != saltB); + assertTrue(LibCloneFactory.effectiveSaltOpen(data, saltA) != LibCloneFactory.effectiveSaltOpen(data, saltB)); + } +} diff --git a/test/src/lib/LibCloneFactory.predictCloneAddress.t.sol b/test/src/lib/LibCloneFactory.predictCloneAddress.t.sol new file mode 100644 index 0000000..f2e1658 --- /dev/null +++ b/test/src/lib/LibCloneFactory.predictCloneAddress.t.sol @@ -0,0 +1,58 @@ +// 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 {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; + +/// @title LibCloneFactoryPredictCloneAddressTest +/// @notice Tests `LibCloneFactory.predictCloneAddress` against two independent +/// oracles: OpenZeppelin's `Clones.predictDeterministicAddress` — a foreign +/// implementation of the same EIP-1167 CREATE2 prediction, so any divergence +/// in our creation-code bytes or hashing shows up as a different address — +/// and the raw CREATE2 formula computed longhand here. +contract LibCloneFactoryPredictCloneAddressTest is Test { + /// Byte-for-byte equivalence with OZ Clones for every (factory, + /// implementation, effectiveSalt): same creation code, same formula, same + /// address. This is the equivalence oracle that lets the deploy half swap + /// its OZ-backed concrete for a delegation into this library. + function testPredictCloneAddressMatchesOZ(address factory, address implementation, bytes32 effectiveSalt) + external + pure + { + assertEq( + LibCloneFactory.predictCloneAddress(factory, implementation, effectiveSalt), + Clones.predictDeterministicAddress(implementation, effectiveSalt, factory) + ); + } + + /// The raw CREATE2 formula, written out longhand: + /// `address(keccak256(0xff ++ factory ++ salt ++ keccak256(creationCode)))` + /// over the EIP-1167 creation bytes written out literally. + function testPredictCloneAddressIsCreate2Formula(address factory, address implementation, bytes32 effectiveSalt) + external + pure + { + bytes memory creationCode = abi.encodePacked( + hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" + ); + address expected = address( + uint160(uint256(keccak256(abi.encodePacked(hex"ff", factory, effectiveSalt, keccak256(creationCode))))) + ); + assertEq(LibCloneFactory.predictCloneAddress(factory, implementation, effectiveSalt), expected); + } + + /// A real CREATE2 deploy of the creation code lands exactly where the + /// prediction says, from a live factory address (this test contract). + function testPredictCloneAddressMatchesRealDeploy(address implementation, bytes32 effectiveSalt) external { + address predicted = LibCloneFactory.predictCloneAddress(address(this), implementation, effectiveSalt); + bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), effectiveSalt) + } + assertEq(child, predicted); + } +} From 5de54c8cd53ea2a70efc2275d22a3916cd2152bd Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 14:40:16 +0000 Subject: [PATCH 2/7] docs: the library half is interfaces plus Lib* logic, not interface-only README, CLAUDE.md and the release workflow all asserted the post-split interface-only shape; LibCloneFactory makes them stale. CLAUDE.md stays under its 4096-byte agent-context cap by cutting, not raising. Co-Authored-By: Claude Fable 5 --- .github/workflows/package-release.yaml | 5 +- CLAUDE.md | 94 +++++++++++++------------- README.md | 19 +++++- 3 files changed, 67 insertions(+), 51 deletions(-) diff --git a/.github/workflows/package-release.yaml b/.github/workflows/package-release.yaml index 9e7608c..41ccfd9 100644 --- a/.github/workflows/package-release.yaml +++ b/.github/workflows/package-release.yaml @@ -7,7 +7,8 @@ jobs: release: uses: rainlanguage/rainix/.github/workflows/rainix-autopublish.yaml@main with: - # Library repo: the ICloneable* interface surface. The deploy half (concrete + - # deploy-pin snapshots) lives in rain.factory.deploy (rainlanguage/rain.factory#46). + # Library repo: the ICloneable* interface surface plus the Lib* logic behind + # it. The deploy half (concrete + deploy-pin snapshots) lives in + # rain.factory.deploy (rainlanguage/rain.factory#46). soldeer-package: rain-factory secrets: inherit diff --git a/CLAUDE.md b/CLAUDE.md index 7d53675..94ba796 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,18 +6,19 @@ code in this repository. ## Project Overview rain.factory is a Solidity **library** repo: the `ICloneable*` interface surface -for EIP1167 minimal proxy (clone) factories in the Rain ecosystem. It is the -library half of the library/deploy split (rainlanguage/rain.factory#46) and -holds interfaces only — no concrete contract, no deploy pins, no deploy script, -and no tests. +for EIP1167 minimal proxy (clone) factories in the Rain ecosystem, plus the +`Lib*` logic that implements it. It is the library half of the library/deploy +split (rainlanguage/rain.factory#46): the salt derivations, guards and the +clone-initialize-verify flow live here as internal library code, unit tested — +no concrete contract, no deploy pins, no deploy script. -The concrete `CloneFactory` that implements these interfaces, its deployed -address + codehash pins (`LibCloneFactoryDeploy`), the frozen -`src/generated//` deploy-pin snapshots and `script/Deploy.sol` all live in -[`rain.factory.deploy`](https://github.com/rainlanguage/rain.factory.deploy) and -publish as the `rain-factory-deploy` Soldeer package. Consumers that need only -the interfaces depend on `rain-factory`; consumers that need the deployed -address/codehash pins depend on `rain-factory-deploy`. +The concrete `CloneFactory` — meant to be a pure delegation into +`LibCloneFactory`, one call per entry point — its deployed address + codehash +pins (`LibCloneFactoryDeploy`), the frozen deploy-pin snapshots and +`script/Deploy.sol` all live in +[`rain.factory.deploy`](https://github.com/rainlanguage/rain.factory.deploy), +publishing as `rain-factory-deploy`. Depend on `rain-factory` for the interfaces +and library; on `rain-factory-deploy` for the deployed pins. License: LicenseRef-DCL-1.0 (DecentraLicense). All source files must include SPDX headers. @@ -26,49 +27,50 @@ SPDX headers. Nix + Foundry. Enter the shell with `nix develop`, then run rainix tasks: `rainix-sol-static` (Slither), `rainix-sol-legal` (REUSE), `rainix-sol-prelude` -(deps, run first), `rainix-sol-test`, and `forge build` directly. There is no -test suite here — the interfaces have no behaviour; the tests that exercise them -live against the concrete in rain.factory.deploy. +(deps, run first), `rainix-sol-test`, and `forge build` / `forge test` directly. +The suite under `test/` unit-tests the library through `TestCloneFactory`, a +pure-delegation concrete, pinning the EIP1167 construction against OZ `Clones` +as a foreign oracle. ## Architecture -- `src/interface/ICloneableV2.sol` — Interface for cloneable contracts. - `initialize(bytes)` must return `ICLONEABLE_V2_SUCCESS` (keccak256 hash) on - success. -- `src/interface/ICloneableFactoryV2.sol` — Legacy factory interface: the - nonce-dependent `clone(address, bytes)` and `NewClone` event. The concrete - factory in rain.factory.deploy implements `ICloneableFactoryV4`, not this; - still published for other consumers. -- `src/interface/ICloneableFactoryV3.sol` — Deterministic-only factory - interface: `cloneDeterministic(address, bytes, bytes32)` + - `predictDeterministicAddress(address, bytes32, address)` (CREATE2, salt - namespaced by `msg.sender`) and its own `NewClone` event. Standalone — does - NOT extend `ICloneableFactoryV2` and has no non-deterministic `clone()`. Still - published for consumers pinned to it. +- `src/interface/ICloneableV2.sol` — Cloneable contracts. `initialize(bytes)` + must return `ICLONEABLE_V2_SUCCESS` on success. +- `src/interface/ICloneableFactoryV2.sol` — Legacy factory interface + (nonce-dependent `clone`). Still published for other consumers. +- `src/interface/ICloneableFactoryV3.sol` — Deterministic-only factory interface + (CREATE2, salt namespaced by `msg.sender`). Standalone — does NOT extend V2; + the non-deterministic `clone()` was intentionally dropped. Still published for + consumers pinned to it. - `src/interface/ICloneableFactoryV4.sol` — Current factory interface. Extends - `ICloneableFactoryV3` and defines the open-salt pair - `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`. Both - derivations are pinned to exact bytes, each `keccak256`-ing a 96-byte preimage - led by a distinct string-derived domain tag, so the two images are disjoint by - construction. The full spec is the NatSpec on the interface. -- `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, - `ICloneableFactoryV1`, `IFactory`). Do not use for new work. + V3 and adds the open-salt pair `cloneDeterministicOpenSalt` / + `predictDeterministicAddressOpenSalt`. Both derivations are pinned to exact + bytes, each `keccak256`-ing a 96-byte preimage led by a distinct + string-derived domain tag, so the two images are disjoint by construction. The + NatSpec on this interface is the spec for `LibCloneFactory`. +- `src/lib/LibCloneFactory.sol` — The whole factory as internal library logic: + both effective-salt derivations, the implementation-code guard, EIP1167 + creation code + CREATE2 prediction (no external cloning dependency), and the + atomic clone-initialize-verify flow with the typed errors and `NewClone`. + `msg.sender` and `address(this)` are read inside the library, so a delegating + concrete cannot misroute them. +- `src/interface/deprecated/` — Legacy (`ICloneableV1`, `ICloneableFactoryV1`, + `IFactory`). Do not use for new work. -`src/` holds nothing else. The interfaces import nothing from outside this repo -— intra-repo inheritance is allowed and `ICloneableFactoryV4` extends -`ICloneableFactoryV3` — which is what makes this half a standalone publish. +The published `src/` imports nothing from outside this repo — which is what +makes this half a standalone publish. ## Solidity Conventions -- Solidity version: every file here is an interface and floats `^` (the - interfaces use `^0.8.18`) so downstream soldeer consumers on a different - `0.8.x` can still compile them. The `=0.8.25` exact-pin rule applies to - concrete contracts, scripts and tests, which live in rain.factory.deploy. +- Versions: interfaces float `^0.8.18` so downstream soldeer consumers on a + different `0.8.x` still compile them; libraries float `^0.8.25`; tests pin + `=0.8.25`, as do concretes and scripts (which live in rain.factory.deploy). +- No named return values. - Compiler (`foundry.toml`): Cancun EVM, optimizer at 100,000 runs, no CBOR metadata (`cbor_metadata = false`, `bytecode_hash = "none"`). - Dependencies are managed with Soldeer (`[dependencies]` in `foundry.toml` + - `soldeer.lock`, vendored under `dependencies/`). The interfaces import nothing - external, so the only entry is forge-std. `@openzeppelin-contracts`, - `rain-extrospection`, `rain-deploy` and `rain-sol-codegen` belong to the - deploy half and must not be added here: adding one means concrete code has - landed in a library repo. + `soldeer.lock`, vendored under `dependencies/`). Everything there is + test-harness only: forge-std, plus `@openzeppelin-contracts` as the + equivalence oracle for the EIP1167 construction. `rain-deploy` and + `rain-sol-codegen` belong to the deploy half and must not be added here: + needing one means deploy-pin code has landed in a library repo. diff --git a/README.md b/README.md index e9ef73f..d94c1bd 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,21 @@ Docs at https://rainprotocol.github.io/rain.factory This repo is the **library** half of the library/deploy split ([#46](https://github.com/rainlanguage/rain.factory/issues/46)): the -`ICloneable*` interface surface, and nothing else. It publishes to Soldeer as -`rain-factory`. +`ICloneable*` interface surface, plus the `Lib*` logic that implements it. It +publishes to Soldeer as `rain-factory`. + +## Library + +`LibCloneFactory` is the whole of an `ICloneableFactoryV4` factory as internal +library code, unit tested here: both effective-salt derivations pinned by that +interface, the implementation-code guard, the EIP1167 creation code and its +CREATE2 address prediction (constructed from the standard's own bytes, so the +published `src/` depends on no external cloning code), and the atomic +clone-initialize-verify flow with its typed errors and the `NewClone` event. +`msg.sender` and `address(this)` are read inside the library, so a concrete +factory is nothing but one delegation per entry point and cannot misroute +either. The tests pin the construction byte for byte against OpenZeppelin +`Clones` as a foreign implementation of the same standard. ## Concrete implementations @@ -17,7 +30,7 @@ together with its deployed address + codehash pins, its frozen per-release deploy-pin snapshots and its deploy script. That repo publishes as `rain-factory-deploy`. -Depend on `rain-factory` if you need only the interfaces. Depend on +Depend on `rain-factory` if you need the interfaces or the library. Depend on `rain-factory-deploy` if you need the deployed address or codehash of a live `CloneFactory`. From cfc224427225a273bf88d89210a167c7efcd3a63 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 14:44:42 +0000 Subject: [PATCH 3/7] chore: pre-commit --all-files formatter output on files predating this branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prettier-rainix, taplo and yamlfmt normalize .vscode/settings.json, slither.config.json, REUSE.toml and .coderabbitai.yaml — all untouched by the feature commits; main fails the all-files run the same way. Mechanical output only, committed so the branch gate runs clean. Co-Authored-By: Claude Fable 5 --- .coderabbitai.yaml | 1 - .vscode/settings.json | 4 ++-- REUSE.toml | 32 ++++++++++++++++---------------- slither.config.json | 4 ++-- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.coderabbitai.yaml b/.coderabbitai.yaml index 63c3ccb..c6e9c57 100644 --- a/.coderabbitai.yaml +++ b/.coderabbitai.yaml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LicenseRef-DCL-1.0 # SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd - reviews: path_filters: - "!audit/**" diff --git a/.vscode/settings.json b/.vscode/settings.json index 3905ae4..9119366 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.rulers": [80] -} \ No newline at end of file + "editor.rulers": [80] +} diff --git a/REUSE.toml b/REUSE.toml index c4a3e48..c111583 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,22 +2,22 @@ version = 1 [[annotations]] path = [ - ".github/workflows/**/", - ".vscode/**/", - ".coderabbitai.yaml", - ".gitignore", - ".gitmodules", - "audit/**/", - "CLAUDE.md", - "README.md", - "flake.lock", - "flake.nix", - "foundry.toml", - "slither.config.json", - "REUSE.toml", - "foundry.lock", - "soldeer.lock", - ".soldeerignore", + ".github/workflows/**/", + ".vscode/**/", + ".coderabbitai.yaml", + ".gitignore", + ".gitmodules", + "audit/**/", + "CLAUDE.md", + "README.md", + "flake.lock", + "flake.nix", + "foundry.toml", + "slither.config.json", + "REUSE.toml", + "foundry.lock", + "soldeer.lock", + ".soldeerignore", ] SPDX-FileCopyrightText = "Copyright (c) 2020 Rain Open Source Software Ltd" SPDX-License-Identifier = "LicenseRef-DCL-1.0" diff --git a/slither.config.json b/slither.config.json index 522c8ae..8d9c84b 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { - "detectors_to_exclude": "assembly-usage,solc-version,pragma,unindexed-event-address", - "filter_paths": "dependencies/forge-std-1.16.1" + "detectors_to_exclude": "assembly-usage,solc-version,pragma,unindexed-event-address", + "filter_paths": "dependencies/forge-std-1.16.1" } From 41febcd3731b35bbc8738a8a897b3eb38b995bf0 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 14:47:58 +0000 Subject: [PATCH 4/7] REUSE: annotate .gas-snapshot New generated file with no header capability; listed in the annotations path array per rain.extrospection's precedent. reuse lint is compliant. Co-Authored-By: Claude Fable 5 --- REUSE.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/REUSE.toml b/REUSE.toml index c111583..88161cf 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,6 +2,7 @@ version = 1 [[annotations]] path = [ + ".gas-snapshot", ".github/workflows/**/", ".vscode/**/", ".coderabbitai.yaml", From d8983e845389c3b4739dff4bc70973ba2ac2ac37 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 15:13:35 +0000 Subject: [PATCH 5/7] refactor: fold the factory logic into LibICloneableFactoryV4 per ruling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruling (thedavidmeister, 2026-08-20): #58's design wins. Its LibICloneableFactoryV4 — the interface-mirroring lib carrying the two effective-salt derivations — is the shape; this branch's LibCloneFactory must not stand next to it as a parallel second lib. So: LibCloneFactory's duplicate derivations (effectiveSaltNamespaced / effectiveSaltOpen) are deleted in favour of the merged effectiveSalt / effectiveOpenSalt, which stay byte-identical to #58. Everything else — the EIP-1167 creation code and CREATE2 prediction, the implementation-code guard, the atomic clone-initialize-verify flow with its typed errors, and the four ICloneableFactoryV4 entry points — folds into LibICloneableFactoryV4 beneath them, delegating to the merged derivations. The internal salt parameter is renamed derivedSalt so it cannot shadow the effectiveSalt function it now sits beside. The lib's pragma moves ^0.8.18 -> ^0.8.25: the flow emits the interface's NewClone via qualified access (ICloneableFactoryV3.NewClone), which needs solc >= 0.8.21, and this repo's convention is that libraries float ^0.8.25 while interfaces keep ^0.8.18. Tests follow the fold: the merged LibICloneableFactoryV4.t.sol gains the six non-duplicate derivation tests (preimage shape and input sensitivity, renamed to the merged function names); the two LibCloneFactory derivation suites are deleted as duplicates of what #58 already covers; the five machinery/flow suites are renamed LibICloneableFactoryV4.*.t.sol and retargeted, as is the TestCloneFactory pure-delegation harness. 42 tests, 6 suites, fuzz 2048, all green; .gas-snapshot regenerated. Co-Authored-By: Claude Fable 5 --- .gas-snapshot | 82 +++---- src/lib/LibCloneFactory.sol | 225 ------------------ src/lib/LibICloneableFactoryV4.sol | 203 +++++++++++++++- test/src/concrete/TestCloneFactory.sol | 12 +- ...CloneFactory.effectiveSaltNamespaced.t.sol | 79 ------ .../LibCloneFactory.effectiveSaltOpen.t.sol | 47 ---- ...leFactoryV4.checkImplementationCode.t.sol} | 14 +- ...loneableFactoryV4.cloneCreationCode.t.sol} | 12 +- ...oneableFactoryV4.cloneDeterministic.t.sol} | 8 +- ...actoryV4.cloneDeterministicOpenSalt.t.sol} | 14 +- ...neableFactoryV4.predictCloneAddress.t.sol} | 16 +- test/src/lib/LibICloneableFactoryV4.t.sol | 55 +++++ 12 files changed, 329 insertions(+), 438 deletions(-) delete mode 100644 src/lib/LibCloneFactory.sol delete mode 100644 test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol delete mode 100644 test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol rename test/src/lib/{LibCloneFactory.checkImplementationCode.t.sol => LibICloneableFactoryV4.checkImplementationCode.t.sol} (74%) rename test/src/lib/{LibCloneFactory.cloneCreationCode.t.sol => LibICloneableFactoryV4.cloneCreationCode.t.sol} (78%) rename test/src/lib/{LibCloneFactory.cloneDeterministic.t.sol => LibICloneableFactoryV4.cloneDeterministic.t.sol} (97%) rename test/src/lib/{LibCloneFactory.cloneDeterministicOpenSalt.t.sol => LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol} (97%) rename test/src/lib/{LibCloneFactory.predictCloneAddress.t.sol => LibICloneableFactoryV4.predictCloneAddress.t.sol} (76%) diff --git a/.gas-snapshot b/.gas-snapshot index 9ed1a4e..8c76c86 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,40 +1,42 @@ -LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) -LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) -LibCloneFactoryCheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) -LibCloneFactoryCloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) -LibCloneFactoryCloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 459468, ~: 443058) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 467283, ~: 450603) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 467902, ~: 451203) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 457008, ~: 438428) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 359284, ~: 349969) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6500, ~: 6482) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455790, ~: 438862) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361912, ~: 352503) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040442913, ~: 1040443151) -LibCloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11124, ~: 11129) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 465161, ~: 448453) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 359262, ~: 349951) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455723, ~: 438823) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361424, ~: 352030) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040443446, ~: 1040443589) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 460393, ~: 444001) -LibCloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11087, ~: 11111) -LibCloneFactoryEffectiveSaltNamespacedTest:testDomainTagsDistinct() (gas: 347) -LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) -LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedDisjointFromOpen(address,bytes32,bytes,bytes32) (runs: 2048, μ: 1363, ~: 1356) -LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedIsSpecEquation(address,bytes32) (runs: 2048, μ: 819, ~: 819) -LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedPreimageShape(address,bytes32) (runs: 2048, μ: 933, ~: 933) -LibCloneFactoryEffectiveSaltNamespacedTest:testEffectiveSaltNamespacedSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3753, ~: 3753) -LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenDataSensitive(bytes,bytes,bytes32) (runs: 2048, μ: 4530, ~: 4525) -LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenIsSpecEquation(bytes,bytes32) (runs: 2048, μ: 1237, ~: 1226) -LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenPreimageShape(bytes,bytes32) (runs: 2048, μ: 1243, ~: 1232) -LibCloneFactoryEffectiveSaltOpenTest:testEffectiveSaltOpenSaltSensitive(bytes,bytes32,bytes32) (runs: 2048, μ: 4092, ~: 4081) -LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) -LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) -LibCloneFactoryPredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) \ No newline at end of file +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 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: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: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: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) +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: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) +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:testDomainTagsDistinct() (gas: 233) +LibICloneableFactoryV4Test:testDomainTagsPinned() (gas: 325) +LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4687, ~: 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: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 diff --git a/src/lib/LibCloneFactory.sol b/src/lib/LibCloneFactory.sol deleted file mode 100644 index 0613662..0000000 --- a/src/lib/LibCloneFactory.sol +++ /dev/null @@ -1,225 +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 "../interface/ICloneableV2.sol"; -import {ICloneableFactoryV3} from "../interface/ICloneableFactoryV3.sol"; -import { - ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, - ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN -} from "../interface/ICloneableFactoryV4.sol"; - -/// Thrown when an implementation has zero code size which is always a mistake: -/// an EIP-1167 proxy of a codeless implementation delegates every call — -/// `initialize` included — to nothing. -error ZeroImplementationCodeSize(); - -/// Thrown when the `CREATE2` deploy of the clone itself fails. With the tiny -/// fixed EIP-1167 initcode the only realistic cause is that the effective salt -/// is already taken: the exact clone asked for is already at the address, so -/// the caller can never mistake an already-initialized contract for their own -/// fresh deploy. -error CloneDeploymentFailed(); - -/// Thrown when initialization fails: `ICloneableV2.initialize` on the fresh -/// clone returned something other than `ICLONEABLE_V2_SUCCESS`. -error InitializationFailed(); - -/// @dev The EIP-1167 creation code up to the implementation address: the -/// 10-byte deploy preamble (which returns the trailing 45 bytes as runtime -/// code) followed by the first 10 bytes of the runtime. -bytes constant EIP1167_CREATION_CODE_PREFIX = hex"3d602d80600a3d3981f3363d3d373d3d3d363d73"; - -/// @dev The EIP-1167 bytes after the implementation address, shared by the -/// creation code and the runtime code. -bytes constant EIP1167_CREATION_CODE_SUFFIX = hex"5af43d82803e903d91602b57fd5bf3"; - -/// @title LibCloneFactory -/// @notice The whole of an `ICloneableFactoryV4` factory as internal library -/// logic, so a concrete factory is nothing but one delegation per entry point. -/// This is the library half of the library/deploy split -/// (rainlanguage/rain.factory#46): the derivations, guards and the -/// clone-initialize-verify flow live here, unit tested; the deploy half's -/// concrete `CloneFactory` adds no behaviour of its own. -/// -/// Both effective-salt derivations are the ones `ICloneableFactoryV4` pins to -/// exact bytes — a `keccak256` over a 96-byte preimage whose FIRST word is a -/// distinct, string-derived domain tag the caller cannot set — and its NatSpec -/// is the spec for everything here: the disjointness of the two images, the -/// atomic clone-and-initialize, and the `NewClone` event carrying the RAW -/// caller salt. -/// -/// `msg.sender` is read INSIDE this library — `cloneDeterministic` namespaces -/// by it and `NewClone` reports it — and the internal functions execute in the -/// factory's own call context, so a delegating concrete cannot get either -/// wrong: there is no sender parameter to misroute `tx.origin` into. Likewise -/// the predictions read `address(this)`, the factory the library is inlined -/// into. -library LibCloneFactory { - /// The effective `CREATE2` salt of the namespaced pair, exactly as - /// `ICloneableFactoryV4` fixes it: - /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt))`. - /// The address this salt produces commits to WHO deployed — nobody but - /// `deployer` can reach it through the factory — and not to what was - /// deployed there. - /// @param deployer The account whose deploy this is: `msg.sender` when - /// deploying, the `deployer` argument when predicting. - /// @param salt The caller-chosen salt. - /// @return The effective `CREATE2` salt. - function effectiveSaltNamespaced(address deployer, bytes32 salt) internal pure returns (bytes32) { - return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)); - } - - /// The effective `CREATE2` salt of the open-salt pair, exactly as - /// `ICloneableFactoryV4` fixes it: - /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. - /// Nothing caller-derived is hashed in, so every account lands on the same - /// address — and because `data` is hashed in, every account that lands - /// there deploys the same contract initialized with the same bytes. - /// @param data The initialization data, part of the address derivation. - /// @param salt The caller-chosen salt. - /// @return The effective `CREATE2` salt. - function effectiveSaltOpen(bytes memory data, bytes32 salt) internal pure returns (bytes32) { - return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); - } - - /// The canonical 55-byte EIP-1167 creation code for `implementation`. - /// Constructed from the standard's bytes directly so this library — and - /// with it the published factory — depends on no external cloning code; - /// the tests pin it byte for byte against OZ `Clones` as a foreign - /// implementation of the same standard. - /// @param implementation The contract the deployed proxy will delegate to. - /// @return The creation code. - function cloneCreationCode(address implementation) internal pure returns (bytes memory) { - return abi.encodePacked(EIP1167_CREATION_CODE_PREFIX, implementation, EIP1167_CREATION_CODE_SUFFIX); - } - - /// The address `CREATE2` gives the EIP-1167 clone of `implementation` from - /// `factory` at `effectiveSalt`: - /// `address(keccak256(0xff ++ factory ++ effectiveSalt ++ keccak256(creationCode)))`. - /// A pure function of its inputs, computable by anyone offchain. - /// @param factory The factory that would deploy the clone. - /// @param implementation The contract to clone. - /// @param effectiveSalt The effective `CREATE2` salt, from one of the two - /// derivations above. - /// @return The predicted clone address. - function predictCloneAddress(address factory, address implementation, bytes32 effectiveSalt) - internal - pure - returns (address) - { - return address( - uint160( - uint256( - keccak256( - abi.encodePacked(hex"ff", factory, effectiveSalt, keccak256(cloneCreationCode(implementation))) - ) - ) - ) - ); - } - - /// Reverts with `ZeroImplementationCodeSize` if `implementation` has no - /// code. Always a mistake: the clone would delegate every call to nothing. - /// @param implementation The contract to clone. - function checkImplementationCode(address implementation) internal view { - if (implementation.code.length == 0) { - revert ZeroImplementationCodeSize(); - } - } - - /// The shared tail of both clone entry points: guard the implementation, - /// `CREATE2` the EIP-1167 clone at `effectiveSalt`, emit `NewClone` with - /// the RAW caller salt, then run the mandatory `ICloneableV2.initialize` - /// check — atomically, with nothing else called on the proxy first, and - /// the clone only considered created if `initialize` returns - /// `ICLONEABLE_V2_SUCCESS`, per the shared spec on - /// `ICloneableFactoryV3.cloneDeterministic`. - /// @param implementation The contract to clone. - /// @param effectiveSalt The effective `CREATE2` salt, from one of the two - /// derivations above. - /// @param data The initialization data, forwarded verbatim to - /// `ICloneableV2.initialize`. - /// @param salt The caller-chosen salt, emitted raw in `NewClone`. - /// @return The deployed and initialized child contract address. - function cloneAndInitialize(address implementation, bytes32 effectiveSalt, bytes memory data, bytes32 salt) - internal - returns (address) - { - checkImplementationCode(implementation); - bytes memory creationCode = cloneCreationCode(implementation); - address child; - assembly ("memory-safe") { - child := create2(0, add(creationCode, 0x20), mload(creationCode), effectiveSalt) - } - if (child == address(0)) { - revert CloneDeploymentFailed(); - } - emit ICloneableFactoryV3.NewClone(msg.sender, implementation, child, salt, data); - // Checking the return value of initialize is mandatory as per - // ICloneableFactoryV3 and ICloneableFactoryV4. - if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) { - revert InitializationFailed(); - } - return child; - } - - /// `ICloneableFactoryV3.cloneDeterministic`, whole: the namespaced - /// derivation over `msg.sender` — read here, not passed, so a delegating - /// concrete cannot namespace by anything else — then the shared - /// clone-initialize-verify flow. - /// @param implementation The contract to clone. - /// @param data As per `ICloneableV2`. - /// @param salt The caller-chosen salt. - /// @return The deployed and initialized child contract address. - function cloneDeterministic(address implementation, bytes memory data, bytes32 salt) internal returns (address) { - return cloneAndInitialize(implementation, effectiveSaltNamespaced(msg.sender, salt), data, salt); - } - - /// `ICloneableFactoryV3.predictDeterministicAddress`, whole: the address - /// `cloneDeterministic(implementation, _, salt)` deploys to when called by - /// `deployer` on the factory this library is inlined into. - /// @param implementation The contract to clone. - /// @param salt The caller-chosen salt. - /// @param deployer The account that will call `cloneDeterministic`. - /// @return The predicted clone address. - function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) - internal - view - returns (address) - { - return predictCloneAddress(address(this), implementation, effectiveSaltNamespaced(deployer, salt)); - } - - /// `ICloneableFactoryV4.cloneDeterministicOpenSalt`, whole: the open-salt - /// derivation — no caller-derived value hashed in — then the shared - /// clone-initialize-verify flow. - /// @param implementation The contract to clone. - /// @param data As per `ICloneableV2`, and part of the address derivation. - /// MAY be empty. - /// @param salt The caller-chosen salt. - /// @return The deployed and initialized child contract address. - function cloneDeterministicOpenSalt(address implementation, bytes memory data, bytes32 salt) - internal - returns (address) - { - return cloneAndInitialize(implementation, effectiveSaltOpen(data, salt), data, salt); - } - - /// `ICloneableFactoryV4.predictDeterministicAddressOpenSalt`, whole: the - /// address `cloneDeterministicOpenSalt(implementation, data, salt)` - /// deploys to from the factory this library is inlined into, whoever calls - /// it. - /// @param implementation The contract to clone. - /// @param data The initialization data that will be passed to - /// `ICloneableV2.initialize`. - /// @param salt The caller-chosen salt. - /// @return The predicted clone address. - function predictDeterministicAddressOpenSalt(address implementation, bytes memory data, bytes32 salt) - internal - view - returns (address) - { - return predictCloneAddress(address(this), implementation, effectiveSaltOpen(data, salt)); - } -} diff --git a/src/lib/LibICloneableFactoryV4.sol b/src/lib/LibICloneableFactoryV4.sol index 2de05c0..d85fe9d 100644 --- a/src/lib/LibICloneableFactoryV4.sol +++ b/src/lib/LibICloneableFactoryV4.sol @@ -1,21 +1,66 @@ // SPDX-License-Identifier: LicenseRef-DCL-1.0 // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.18; +pragma solidity ^0.8.25; +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "../interface/ICloneableV2.sol"; +import {ICloneableFactoryV3} from "../interface/ICloneableFactoryV3.sol"; import { ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN } from "../interface/ICloneableFactoryV4.sol"; +/// Thrown when an implementation has zero code size which is always a mistake: +/// an EIP-1167 proxy of a codeless implementation delegates every call — +/// `initialize` included — to nothing. +error ZeroImplementationCodeSize(); + +/// Thrown when the `CREATE2` deploy of the clone itself fails. With the tiny +/// fixed EIP-1167 initcode the only realistic cause is that the effective salt +/// is already taken: the exact clone asked for is already at the address, so +/// the caller can never mistake an already-initialized contract for their own +/// fresh deploy. +error CloneDeploymentFailed(); + +/// Thrown when initialization fails: `ICloneableV2.initialize` on the fresh +/// clone returned something other than `ICLONEABLE_V2_SUCCESS`. +error InitializationFailed(); + +/// @dev The EIP-1167 creation code up to the implementation address: the +/// 10-byte deploy preamble (which returns the trailing 45 bytes as runtime +/// code) followed by the first 10 bytes of the runtime. +bytes constant EIP1167_CREATION_CODE_PREFIX = hex"3d602d80600a3d3981f3363d3d373d3d3d363d73"; + +/// @dev The EIP-1167 bytes after the implementation address, shared by the +/// creation code and the runtime code. +bytes constant EIP1167_CREATION_CODE_SUFFIX = hex"5af43d82803e903d91602b57fd5bf3"; + /// @title LibICloneableFactoryV4 -/// @notice The executable form of the two effective-`CREATE2`-salt derivations -/// that `ICloneableFactoryV4` pins to exact bytes, so a factory, an indexer or a -/// consumer predicting a clone address computes them from one place instead of -/// re-deriving the formulas inline. Each function reproduces its interface -/// formula byte for byte and reads its domain tag from the interface, so the -/// tags have a single source of truth and the derivation cannot drift from the -/// spec. See `ICloneableFactoryV4` for what each salt commits to and why the two -/// images are disjoint. +/// @notice The whole of an `ICloneableFactoryV4` factory as internal library +/// logic, so a concrete factory is nothing but one delegation per entry point. +/// This is the library half of the library/deploy split +/// (rainlanguage/rain.factory#46): the derivations, guards and the +/// clone-initialize-verify flow live here, unit tested; the deploy half's +/// concrete `CloneFactory` adds no behaviour of its own. +/// +/// The library opens with the executable form of the two +/// effective-`CREATE2`-salt derivations that `ICloneableFactoryV4` pins to +/// exact bytes, so a factory, an indexer or a consumer predicting a clone +/// address computes them from one place instead of re-deriving the formulas +/// inline. Each function reproduces its interface formula byte for byte and +/// reads its domain tag from the interface, so the tags have a single source of +/// truth and the derivation cannot drift from the spec. The entry points below +/// them consume the derivations from here and nowhere else. See +/// `ICloneableFactoryV4` for what each salt commits to and why the two images +/// are disjoint — its NatSpec, with the atomic clone-and-initialize and the +/// `NewClone` event carrying the RAW caller salt, is the spec for everything +/// here. +/// +/// `msg.sender` is read INSIDE this library — `cloneDeterministic` namespaces +/// by it and `NewClone` reports it — and the internal functions execute in the +/// factory's own call context, so a delegating concrete cannot get either +/// wrong: there is no sender parameter to misroute `tx.origin` into. Likewise +/// the predictions read `address(this)`, the factory the library is inlined +/// into. library LibICloneableFactoryV4 { /// The effective `CREATE2` salt for the namespaced derivation /// (`cloneDeterministic` / `predictDeterministicAddress`): the caller-chosen @@ -42,4 +87,144 @@ library LibICloneableFactoryV4 { function effectiveOpenSalt(bytes32 salt, bytes memory data) internal pure returns (bytes32) { return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); } + + /// The canonical 55-byte EIP-1167 creation code for `implementation`. + /// Constructed from the standard's bytes directly so this library — and + /// with it the published factory — depends on no external cloning code; + /// the tests pin it byte for byte against OZ `Clones` as a foreign + /// implementation of the same standard. + /// @param implementation The contract the deployed proxy will delegate to. + /// @return The creation code. + function cloneCreationCode(address implementation) internal pure returns (bytes memory) { + return abi.encodePacked(EIP1167_CREATION_CODE_PREFIX, implementation, EIP1167_CREATION_CODE_SUFFIX); + } + + /// The address `CREATE2` gives the EIP-1167 clone of `implementation` from + /// `factory` at `derivedSalt`: + /// `address(keccak256(0xff ++ factory ++ derivedSalt ++ keccak256(creationCode)))`. + /// A pure function of its inputs, computable by anyone offchain. + /// @param factory The factory that would deploy the clone. + /// @param implementation The contract to clone. + /// @param derivedSalt The effective `CREATE2` salt, from `effectiveSalt` or + /// `effectiveOpenSalt`. + /// @return The predicted clone address. + function predictCloneAddress(address factory, address implementation, bytes32 derivedSalt) + internal + pure + returns (address) + { + return address( + uint160( + uint256( + keccak256( + abi.encodePacked(hex"ff", factory, derivedSalt, keccak256(cloneCreationCode(implementation))) + ) + ) + ) + ); + } + + /// Reverts with `ZeroImplementationCodeSize` if `implementation` has no + /// code. Always a mistake: the clone would delegate every call to nothing. + /// @param implementation The contract to clone. + function checkImplementationCode(address implementation) internal view { + if (implementation.code.length == 0) { + revert ZeroImplementationCodeSize(); + } + } + + /// The shared tail of both clone entry points: guard the implementation, + /// `CREATE2` the EIP-1167 clone at `derivedSalt`, emit `NewClone` with + /// the RAW caller salt, then run the mandatory `ICloneableV2.initialize` + /// check — atomically, with nothing else called on the proxy first, and + /// the clone only considered created if `initialize` returns + /// `ICLONEABLE_V2_SUCCESS`, per the shared spec on + /// `ICloneableFactoryV3.cloneDeterministic`. + /// @param implementation The contract to clone. + /// @param derivedSalt The effective `CREATE2` salt, from `effectiveSalt` or + /// `effectiveOpenSalt`. + /// @param data The initialization data, forwarded verbatim to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt, emitted raw in `NewClone`. + /// @return The deployed and initialized child contract address. + function cloneAndInitialize(address implementation, bytes32 derivedSalt, bytes memory data, bytes32 salt) + internal + returns (address) + { + checkImplementationCode(implementation); + bytes memory creationCode = cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), derivedSalt) + } + if (child == address(0)) { + revert CloneDeploymentFailed(); + } + emit ICloneableFactoryV3.NewClone(msg.sender, implementation, child, salt, data); + // Checking the return value of initialize is mandatory as per + // ICloneableFactoryV3 and ICloneableFactoryV4. + if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) { + revert InitializationFailed(); + } + return child; + } + + /// `ICloneableFactoryV3.cloneDeterministic`, whole: `effectiveSalt` over + /// `msg.sender` — read here, not passed, so a delegating concrete cannot + /// namespace by anything else — then the shared clone-initialize-verify + /// flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministic(address implementation, bytes memory data, bytes32 salt) internal returns (address) { + return cloneAndInitialize(implementation, effectiveSalt(msg.sender, salt), data, salt); + } + + /// `ICloneableFactoryV3.predictDeterministicAddress`, whole: the address + /// `cloneDeterministic(implementation, _, salt)` deploys to when called by + /// `deployer` on the factory this library is inlined into. + /// @param implementation The contract to clone. + /// @param salt The caller-chosen salt. + /// @param deployer The account that will call `cloneDeterministic`. + /// @return The predicted clone address. + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveSalt(deployer, salt)); + } + + /// `ICloneableFactoryV4.cloneDeterministicOpenSalt`, whole: + /// `effectiveOpenSalt` — no caller-derived value hashed in — then the + /// shared clone-initialize-verify flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`, and part of the address derivation. + /// MAY be empty. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministicOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + returns (address) + { + return cloneAndInitialize(implementation, effectiveOpenSalt(salt, data), data, salt); + } + + /// `ICloneableFactoryV4.predictDeterministicAddressOpenSalt`, whole: the + /// address `cloneDeterministicOpenSalt(implementation, data, salt)` + /// deploys to from the factory this library is inlined into, whoever calls + /// it. + /// @param implementation The contract to clone. + /// @param data The initialization data that will be passed to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt. + /// @return The predicted clone address. + function predictDeterministicAddressOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveOpenSalt(salt, data)); + } } diff --git a/test/src/concrete/TestCloneFactory.sol b/test/src/concrete/TestCloneFactory.sol index f3bc375..e79231f 100644 --- a/test/src/concrete/TestCloneFactory.sol +++ b/test/src/concrete/TestCloneFactory.sol @@ -7,12 +7,12 @@ pragma solidity =0.8.25; // them, so the tag must name V3 and V3 must be in scope here. import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; import {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol"; -import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; /// @title TestCloneFactory /// @notice A concrete `ICloneableFactoryV4` written the way the deploy half is /// meant to write one: every function is a single delegation into -/// `LibCloneFactory` and nothing else. It exists so the flow tests exercise the +/// `LibICloneableFactoryV4` and nothing else. It exists so the flow tests exercise the /// library through a real external surface — `msg.sender` namespacing and the /// `NewClone` event are observable only across an external call — and it /// doubles as the executable proof that the library surface suffices for a @@ -20,7 +20,7 @@ import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; contract TestCloneFactory is ICloneableFactoryV4 { /// @inheritdoc ICloneableFactoryV3 function cloneDeterministic(address implementation, bytes calldata data, bytes32 salt) external returns (address) { - return LibCloneFactory.cloneDeterministic(implementation, data, salt); + return LibICloneableFactoryV4.cloneDeterministic(implementation, data, salt); } /// @inheritdoc ICloneableFactoryV3 @@ -29,7 +29,7 @@ contract TestCloneFactory is ICloneableFactoryV4 { view returns (address) { - return LibCloneFactory.predictDeterministicAddress(implementation, salt, deployer); + return LibICloneableFactoryV4.predictDeterministicAddress(implementation, salt, deployer); } /// @inheritdoc ICloneableFactoryV4 @@ -37,7 +37,7 @@ contract TestCloneFactory is ICloneableFactoryV4 { external returns (address) { - return LibCloneFactory.cloneDeterministicOpenSalt(implementation, data, salt); + return LibICloneableFactoryV4.cloneDeterministicOpenSalt(implementation, data, salt); } /// @inheritdoc ICloneableFactoryV4 @@ -46,6 +46,6 @@ contract TestCloneFactory is ICloneableFactoryV4 { view returns (address) { - return LibCloneFactory.predictDeterministicAddressOpenSalt(implementation, data, salt); + return LibICloneableFactoryV4.predictDeterministicAddressOpenSalt(implementation, data, salt); } } diff --git a/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol b/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol deleted file mode 100644 index 07ef004..0000000 --- a/test/src/lib/LibCloneFactory.effectiveSaltNamespaced.t.sol +++ /dev/null @@ -1,79 +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 { - ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, - ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN -} from "src/interface/ICloneableFactoryV4.sol"; -import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; - -/// @title LibCloneFactoryEffectiveSaltNamespacedTest -/// @notice Tests `LibCloneFactory.effectiveSaltNamespaced` against the -/// derivation `ICloneableFactoryV4` pins to exact bytes: -/// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))`. -/// The expected values are restated from the interface NatSpec, not read back -/// from the library, so any drift in the library's arithmetic fails here. -contract LibCloneFactoryEffectiveSaltNamespacedTest is Test { - /// The derivation is exactly the spec equation. - function testEffectiveSaltNamespacedIsSpecEquation(address deployer, bytes32 salt) external pure { - assertEq( - LibCloneFactory.effectiveSaltNamespaced(deployer, salt), - keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)) - ); - } - - /// The preimage is 96 bytes whose FIRST word is the string-derived - /// namespaced tag. Packed independently of both the library and the - /// interface constant, so this also pins the constant to its documented - /// string. - function testEffectiveSaltNamespacedPreimageShape(address deployer, bytes32 salt) external pure { - bytes memory preimage = - abi.encodePacked(keccak256("rain.factory.clone.namespaced"), bytes32(uint256(uint160(deployer))), salt); - assertEq(preimage.length, 96); - assertEq(LibCloneFactory.effectiveSaltNamespaced(deployer, salt), keccak256(preimage)); - } - - /// The deployer is in the derivation: two deployers, two salts. - function testEffectiveSaltNamespacedDeployerSensitive(address alice, address bob, bytes32 salt) external pure { - vm.assume(alice != bob); - assertTrue( - LibCloneFactory.effectiveSaltNamespaced(alice, salt) != LibCloneFactory.effectiveSaltNamespaced(bob, salt) - ); - } - - /// The caller salt is in the derivation: two salts, two effective salts. - function testEffectiveSaltNamespacedSaltSensitive(address deployer, bytes32 saltA, bytes32 saltB) external pure { - vm.assume(saltA != saltB); - assertTrue( - LibCloneFactory.effectiveSaltNamespaced(deployer, saltA) - != LibCloneFactory.effectiveSaltNamespaced(deployer, saltB) - ); - } - - /// The two domain tags are distinct words, each pinned to its documented - /// string. This is the anchor of the image disjointness: everything else - /// about the two preimages is caller-chosen, the first word is not. - function testDomainTagsDistinct() external pure { - assertEq(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, keccak256("rain.factory.clone.namespaced")); - assertEq(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, keccak256("rain.factory.clone.opensalt")); - assertTrue(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN != ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN); - } - - /// The namespaced image is disjoint from the open-salt image under freely - /// varying inputs on both sides: the first preimage word differs by - /// construction, so equality of the hashes would be a keccak256 collision. - function testEffectiveSaltNamespacedDisjointFromOpen( - address deployer, - bytes32 namespacedSalt, - bytes memory data, - bytes32 openSalt - ) external pure { - assertTrue( - LibCloneFactory.effectiveSaltNamespaced(deployer, namespacedSalt) - != LibCloneFactory.effectiveSaltOpen(data, openSalt) - ); - } -} diff --git a/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol b/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol deleted file mode 100644 index 550f4bf..0000000 --- a/test/src/lib/LibCloneFactory.effectiveSaltOpen.t.sol +++ /dev/null @@ -1,47 +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 {ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; -import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; - -/// @title LibCloneFactoryEffectiveSaltOpenTest -/// @notice Tests `LibCloneFactory.effectiveSaltOpen` against the derivation -/// `ICloneableFactoryV4` pins to exact bytes: -/// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. -/// The expected values are restated from the interface NatSpec, not read back -/// from the library. -contract LibCloneFactoryEffectiveSaltOpenTest is Test { - /// The derivation is exactly the spec equation. - function testEffectiveSaltOpenIsSpecEquation(bytes memory data, bytes32 salt) external pure { - assertEq( - LibCloneFactory.effectiveSaltOpen(data, salt), - keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))) - ); - } - - /// The preimage is 96 bytes whose FIRST word is the string-derived - /// open-salt tag, whose second is the caller salt and whose third is - /// `keccak256(data)` — `data` enters by hash, so the preimage is fixed - /// length for any data length. Packed independently of both the library - /// and the interface constant. - function testEffectiveSaltOpenPreimageShape(bytes memory data, bytes32 salt) external pure { - bytes memory preimage = abi.encodePacked(keccak256("rain.factory.clone.opensalt"), salt, keccak256(data)); - assertEq(preimage.length, 96); - assertEq(LibCloneFactory.effectiveSaltOpen(data, salt), keccak256(preimage)); - } - - /// `data` is in the derivation: two data, two effective salts. - function testEffectiveSaltOpenDataSensitive(bytes memory dataA, bytes memory dataB, bytes32 salt) external pure { - vm.assume(keccak256(dataA) != keccak256(dataB)); - assertTrue(LibCloneFactory.effectiveSaltOpen(dataA, salt) != LibCloneFactory.effectiveSaltOpen(dataB, salt)); - } - - /// The caller salt is in the derivation: two salts, two effective salts. - function testEffectiveSaltOpenSaltSensitive(bytes memory data, bytes32 saltA, bytes32 saltB) external pure { - vm.assume(saltA != saltB); - assertTrue(LibCloneFactory.effectiveSaltOpen(data, saltA) != LibCloneFactory.effectiveSaltOpen(data, saltB)); - } -} diff --git a/test/src/lib/LibCloneFactory.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol similarity index 74% rename from test/src/lib/LibCloneFactory.checkImplementationCode.t.sol rename to test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index 8618697..07f8ce4 100644 --- a/test/src/lib/LibCloneFactory.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -4,19 +4,19 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCloneFactory, ZeroImplementationCodeSize} from "src/lib/LibCloneFactory.sol"; +import {LibICloneableFactoryV4, ZeroImplementationCodeSize} from "src/lib/LibICloneableFactoryV4.sol"; import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -/// @title LibCloneFactoryCheckImplementationCodeTest -/// @notice Tests `LibCloneFactory.checkImplementationCode`: a codeless +/// @title LibICloneableFactoryV4CheckImplementationCodeTest +/// @notice Tests `LibICloneableFactoryV4.checkImplementationCode`: a codeless /// implementation is always a mistake — its clone would delegate every call, /// `initialize` included, to nothing — so it reverts with a typed error /// before any deploy happens. -contract LibCloneFactoryCheckImplementationCodeTest is Test { +contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { /// External wrapper so `vm.expectRevert` sees the internal library call at /// its own call depth. function checkImplementationCodeExternal(address implementation) external view { - LibCloneFactory.checkImplementationCode(implementation); + LibICloneableFactoryV4.checkImplementationCode(implementation); } /// Any address without code reverts `ZeroImplementationCodeSize`. @@ -29,7 +29,7 @@ contract LibCloneFactoryCheckImplementationCodeTest is Test { /// A deployed contract passes. function testCheckImplementationCodeContract() external { TestCloneable implementation = new TestCloneable(); - LibCloneFactory.checkImplementationCode(address(implementation)); + LibICloneableFactoryV4.checkImplementationCode(address(implementation)); } /// Any nonempty code is enough to pass: the guard is a code-size check, @@ -39,6 +39,6 @@ contract LibCloneFactoryCheckImplementationCodeTest is Test { vm.assume(uint160(implementation) > 0x0a); vm.assume(code.length > 0); vm.etch(implementation, code); - LibCloneFactory.checkImplementationCode(implementation); + LibICloneableFactoryV4.checkImplementationCode(implementation); } } diff --git a/test/src/lib/LibCloneFactory.cloneCreationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol similarity index 78% rename from test/src/lib/LibCloneFactory.cloneCreationCode.t.sol rename to test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol index e514730..b202e39 100644 --- a/test/src/lib/LibCloneFactory.cloneCreationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol @@ -4,13 +4,13 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; -/// @title LibCloneFactoryCloneCreationCodeTest -/// @notice Tests `LibCloneFactory.cloneCreationCode` against the EIP-1167 +/// @title LibICloneableFactoryV4CloneCreationCodeTest +/// @notice Tests `LibICloneableFactoryV4.cloneCreationCode` against the EIP-1167 /// bytes written out literally here, from the EIP, so the library's constants /// are pinned against the standard rather than against themselves. -contract LibCloneFactoryCloneCreationCodeTest is Test { +contract LibICloneableFactoryV4CloneCreationCodeTest is Test { /// The creation code is the canonical 55-byte EIP-1167 sequence: the /// 10-byte deploy preamble, the 10-byte runtime prefix, the 20-byte /// implementation address and the 15-byte runtime suffix. @@ -18,7 +18,7 @@ contract LibCloneFactoryCloneCreationCodeTest is Test { bytes memory expected = abi.encodePacked( hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" ); - bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); assertEq(creationCode.length, 55); assertEq(creationCode, expected); } @@ -28,7 +28,7 @@ contract LibCloneFactoryCloneCreationCodeTest is Test { /// preamble. This pins the preamble's semantics (codecopy of the trailing /// 45 bytes) and not just its bytes. function testCloneCreationCodeDeploysEIP1167Runtime(address implementation, bytes32 salt) external { - bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); address child; assembly ("memory-safe") { child := create2(0, add(creationCode, 0x20), mload(creationCode), salt) diff --git a/test/src/lib/LibCloneFactory.cloneDeterministic.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol similarity index 97% rename from test/src/lib/LibCloneFactory.cloneDeterministic.t.sol rename to test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol index 72a45e0..fa0f27e 100644 --- a/test/src/lib/LibCloneFactory.cloneDeterministic.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -7,19 +7,19 @@ import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; import {ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; -import {CloneDeploymentFailed, InitializationFailed, ZeroImplementationCodeSize} from "src/lib/LibCloneFactory.sol"; +import {CloneDeploymentFailed, 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"; -/// @title LibCloneFactoryCloneDeterministicTest -/// @notice Tests `LibCloneFactory.cloneDeterministic` / +/// @title LibICloneableFactoryV4CloneDeterministicTest +/// @notice Tests `LibICloneableFactoryV4.cloneDeterministic` / /// `predictDeterministicAddress` — the namespaced pair — through /// `TestCloneFactory`, a pure-delegation concrete, because `msg.sender` /// namespacing and the `NewClone` event only exist across an external call. /// The defining property is that the address commits to WHO deployed — /// `(deployer, salt)` — and not to WHAT was initialized. -contract LibCloneFactoryCloneDeterministicTest is Test { +contract LibICloneableFactoryV4CloneDeterministicTest is Test { /// The `TestCloneFactory` instance under test. Stateless, so reused /// everywhere. TestCloneFactory internal immutable I_CLONE_FACTORY; diff --git a/test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol similarity index 97% rename from test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol rename to test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol index f17a96b..1e59618 100644 --- a/test/src/lib/LibCloneFactory.cloneDeterministicOpenSalt.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -11,17 +11,17 @@ import { ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN } from "src/interface/ICloneableFactoryV4.sol"; import { - LibCloneFactory, + LibICloneableFactoryV4, CloneDeploymentFailed, InitializationFailed, ZeroImplementationCodeSize -} from "src/lib/LibCloneFactory.sol"; +} 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"; -/// @title LibCloneFactoryCloneDeterministicOpenSaltTest -/// @notice Tests `LibCloneFactory.cloneDeterministicOpenSalt` / +/// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest +/// @notice Tests `LibICloneableFactoryV4.cloneDeterministicOpenSalt` / /// `predictDeterministicAddressOpenSalt` through `TestCloneFactory`, a /// pure-delegation concrete. The defining property is that the address commits /// to WHAT is deployed — `(implementation, data, salt)` — and to nothing about @@ -29,7 +29,7 @@ import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; /// guarantees. So the two derivations are also tested against each other here, /// including the one squat that the pair of distinct domain tags exists to /// close. -contract LibCloneFactoryCloneDeterministicOpenSaltTest is Test { +contract LibICloneableFactoryV4CloneDeterministicOpenSaltTest is Test { /// The `TestCloneFactory` instance under test. Stateless, so reused /// everywhere. TestCloneFactory internal immutable I_CLONE_FACTORY; @@ -230,7 +230,7 @@ contract LibCloneFactoryCloneDeterministicOpenSaltTest is Test { keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, attacker, attackerSalt)); assertEq( sharedTagCounterfactual, - LibCloneFactory.effectiveSaltOpen(data, openSalt), + LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data), "the attacker's words re-tagged ARE the open effective salt" ); @@ -242,7 +242,7 @@ contract LibCloneFactoryCloneDeterministicOpenSaltTest is Test { keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, openSalt, keccak256(data))); assertEq( sharedTagCounterfactualNamespaced, - LibCloneFactory.effectiveSaltNamespaced(attacker, attackerSalt), + LibICloneableFactoryV4.effectiveSalt(attacker, attackerSalt), "the open words re-tagged ARE the namespaced effective salt" ); diff --git a/test/src/lib/LibCloneFactory.predictCloneAddress.t.sol b/test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol similarity index 76% rename from test/src/lib/LibCloneFactory.predictCloneAddress.t.sol rename to test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol index f2e1658..83e37e0 100644 --- a/test/src/lib/LibCloneFactory.predictCloneAddress.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol @@ -5,15 +5,15 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; -import {LibCloneFactory} from "src/lib/LibCloneFactory.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; -/// @title LibCloneFactoryPredictCloneAddressTest -/// @notice Tests `LibCloneFactory.predictCloneAddress` against two independent +/// @title LibICloneableFactoryV4PredictCloneAddressTest +/// @notice Tests `LibICloneableFactoryV4.predictCloneAddress` against two independent /// oracles: OpenZeppelin's `Clones.predictDeterministicAddress` — a foreign /// implementation of the same EIP-1167 CREATE2 prediction, so any divergence /// in our creation-code bytes or hashing shows up as a different address — /// and the raw CREATE2 formula computed longhand here. -contract LibCloneFactoryPredictCloneAddressTest is Test { +contract LibICloneableFactoryV4PredictCloneAddressTest is Test { /// Byte-for-byte equivalence with OZ Clones for every (factory, /// implementation, effectiveSalt): same creation code, same formula, same /// address. This is the equivalence oracle that lets the deploy half swap @@ -23,7 +23,7 @@ contract LibCloneFactoryPredictCloneAddressTest is Test { pure { assertEq( - LibCloneFactory.predictCloneAddress(factory, implementation, effectiveSalt), + LibICloneableFactoryV4.predictCloneAddress(factory, implementation, effectiveSalt), Clones.predictDeterministicAddress(implementation, effectiveSalt, factory) ); } @@ -41,14 +41,14 @@ contract LibCloneFactoryPredictCloneAddressTest is Test { address expected = address( uint160(uint256(keccak256(abi.encodePacked(hex"ff", factory, effectiveSalt, keccak256(creationCode))))) ); - assertEq(LibCloneFactory.predictCloneAddress(factory, implementation, effectiveSalt), expected); + assertEq(LibICloneableFactoryV4.predictCloneAddress(factory, implementation, effectiveSalt), expected); } /// A real CREATE2 deploy of the creation code lands exactly where the /// prediction says, from a live factory address (this test contract). function testPredictCloneAddressMatchesRealDeploy(address implementation, bytes32 effectiveSalt) external { - address predicted = LibCloneFactory.predictCloneAddress(address(this), implementation, effectiveSalt); - bytes memory creationCode = LibCloneFactory.cloneCreationCode(implementation); + address predicted = LibICloneableFactoryV4.predictCloneAddress(address(this), implementation, effectiveSalt); + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); address child; assembly ("memory-safe") { child := create2(0, add(creationCode, 0x20), mload(creationCode), effectiveSalt) diff --git a/test/src/lib/LibICloneableFactoryV4.t.sol b/test/src/lib/LibICloneableFactoryV4.t.sol index e98d08a..bde71c8 100644 --- a/test/src/lib/LibICloneableFactoryV4.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.t.sol @@ -67,4 +67,59 @@ contract LibICloneableFactoryV4Test is Test { != LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data) ); } + + /// The namespaced preimage is 96 bytes whose FIRST word is the + /// string-derived namespaced tag. Packed independently of both the library + /// and the interface constant, so this also pins the constant to its + /// documented string. + function testEffectiveSaltPreimageShape(address deployer, bytes32 salt) external pure { + bytes memory preimage = + abi.encodePacked(keccak256("rain.factory.clone.namespaced"), bytes32(uint256(uint160(deployer))), salt); + assertEq(preimage.length, 96); + assertEq(LibICloneableFactoryV4.effectiveSalt(deployer, salt), keccak256(preimage)); + } + + /// The deployer is in the namespaced derivation: two deployers, two + /// effective salts. + function testEffectiveSaltDeployerSensitive(address alice, address bob, bytes32 salt) external pure { + vm.assume(alice != bob); + assertTrue(LibICloneableFactoryV4.effectiveSalt(alice, salt) != LibICloneableFactoryV4.effectiveSalt(bob, salt)); + } + + /// The caller salt is in the namespaced derivation: two salts, two + /// effective salts. + function testEffectiveSaltSaltSensitive(address deployer, bytes32 saltA, bytes32 saltB) external pure { + vm.assume(saltA != saltB); + assertTrue( + LibICloneableFactoryV4.effectiveSalt(deployer, saltA) != LibICloneableFactoryV4.effectiveSalt(deployer, saltB) + ); + } + + /// The open-salt preimage is 96 bytes whose FIRST word is the + /// string-derived open-salt tag, whose second is the caller salt and whose + /// third is `keccak256(data)` — `data` enters by hash, so the preimage is + /// fixed length for any data length. Packed independently of both the + /// library and the interface constant. + function testEffectiveOpenSaltPreimageShape(bytes32 salt, bytes memory data) external pure { + bytes memory preimage = abi.encodePacked(keccak256("rain.factory.clone.opensalt"), salt, keccak256(data)); + assertEq(preimage.length, 96); + assertEq(LibICloneableFactoryV4.effectiveOpenSalt(salt, data), keccak256(preimage)); + } + + /// `data` is in the open-salt derivation: two data, two effective salts. + function testEffectiveOpenSaltDataSensitive(bytes32 salt, bytes memory dataA, bytes memory dataB) external pure { + vm.assume(keccak256(dataA) != keccak256(dataB)); + assertTrue( + LibICloneableFactoryV4.effectiveOpenSalt(salt, dataA) != LibICloneableFactoryV4.effectiveOpenSalt(salt, dataB) + ); + } + + /// The caller salt is in the open-salt derivation: two salts, two + /// effective salts. + function testEffectiveOpenSaltSaltSensitive(bytes32 saltA, bytes32 saltB, bytes memory data) external pure { + vm.assume(saltA != saltB); + assertTrue( + LibICloneableFactoryV4.effectiveOpenSalt(saltA, data) != LibICloneableFactoryV4.effectiveOpenSalt(saltB, data) + ); + } } From 69d56dad922508e458b727ea958edf1cb1266d04 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 15:22:50 +0000 Subject: [PATCH 6/7] docs: rewrap CLAUDE.md and README to the fold's phrasing Line-wrapping only; no content change. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 4 ++-- README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 177ae32..30b9ff9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,8 @@ code in this repository. rain.factory is a Solidity **library** repo for EIP1167 minimal proxy (clone) factories in the Rain ecosystem: the `ICloneable*` interface surface and the `LibICloneableFactoryV4` library that implements it. It is the library half of -the library/deploy split (rainlanguage/rain.factory#46) — no concrete -contract, no deploy pins, no deploy script. +the library/deploy split (rainlanguage/rain.factory#46) — no concrete contract, +no deploy pins, no deploy script. The concrete `CloneFactory` — meant to be a pure delegation into `LibICloneableFactoryV4`, one call per entry point — its deployed address + diff --git a/README.md b/README.md index 37d41f8..06832d6 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ derivations the interface pins to exact bytes — the `msg.sender`-namespaced on and the open-salt one — are pure functions that import the domain tags from the interface, so the tags have a single source of truth and a factory, an indexer or a consumer predicting a clone address computes the salt from one place. On -top of them sit the implementation-code guard, the EIP1167 creation code and -its CREATE2 address prediction (constructed from the standard's own bytes, so -the published `src/` depends on no external cloning code), and the atomic +top of them sit the implementation-code guard, the EIP1167 creation code and its +CREATE2 address prediction (constructed from the standard's own bytes, so the +published `src/` depends on no external cloning code), and the atomic clone-initialize-verify flow with its typed errors and the `NewClone` event. `msg.sender` and `address(this)` are read inside the library, so a concrete factory is nothing but one delegation per entry point and cannot misroute From f05d5e7a3961e9aa8c27ef510fdde82a0a7e8ce4 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Thu, 20 Aug 2026 15:30:26 +0000 Subject: [PATCH 7/7] forge fmt the renamed test suites CI's static gate runs forge fmt --check, which this repo's pre-commit does not; formatted with the pinned toolchain. Co-Authored-By: Claude Fable 5 --- .../lib/LibICloneableFactoryV4.cloneDeterministic.t.sol | 6 +++++- test/src/lib/LibICloneableFactoryV4.t.sol | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol index fa0f27e..8d82b5f 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -7,7 +7,11 @@ import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; import {ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; -import {CloneDeploymentFailed, InitializationFailed, ZeroImplementationCodeSize} from "src/lib/LibICloneableFactoryV4.sol"; +import { + CloneDeploymentFailed, + 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"; diff --git a/test/src/lib/LibICloneableFactoryV4.t.sol b/test/src/lib/LibICloneableFactoryV4.t.sol index bde71c8..06bd1c6 100644 --- a/test/src/lib/LibICloneableFactoryV4.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.t.sol @@ -91,7 +91,8 @@ contract LibICloneableFactoryV4Test is Test { function testEffectiveSaltSaltSensitive(address deployer, bytes32 saltA, bytes32 saltB) external pure { vm.assume(saltA != saltB); assertTrue( - LibICloneableFactoryV4.effectiveSalt(deployer, saltA) != LibICloneableFactoryV4.effectiveSalt(deployer, saltB) + LibICloneableFactoryV4.effectiveSalt(deployer, saltA) + != LibICloneableFactoryV4.effectiveSalt(deployer, saltB) ); } @@ -110,7 +111,8 @@ contract LibICloneableFactoryV4Test is Test { function testEffectiveOpenSaltDataSensitive(bytes32 salt, bytes memory dataA, bytes memory dataB) external pure { vm.assume(keccak256(dataA) != keccak256(dataB)); assertTrue( - LibICloneableFactoryV4.effectiveOpenSalt(salt, dataA) != LibICloneableFactoryV4.effectiveOpenSalt(salt, dataB) + LibICloneableFactoryV4.effectiveOpenSalt(salt, dataA) + != LibICloneableFactoryV4.effectiveOpenSalt(salt, dataB) ); } @@ -119,7 +121,8 @@ contract LibICloneableFactoryV4Test is Test { function testEffectiveOpenSaltSaltSensitive(bytes32 saltA, bytes32 saltB, bytes memory data) external pure { vm.assume(saltA != saltB); assertTrue( - LibICloneableFactoryV4.effectiveOpenSalt(saltA, data) != LibICloneableFactoryV4.effectiveOpenSalt(saltB, data) + LibICloneableFactoryV4.effectiveOpenSalt(saltA, data) + != LibICloneableFactoryV4.effectiveOpenSalt(saltB, data) ); } }