diff --git a/.gas-snapshot b/.gas-snapshot index 8c76c86..6a17e50 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,29 +1,48 @@ -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) +ICloneableV2Test:testCloneableV2SuccessDistinctFromDomainTags() (gas: 279) +ICloneableV2Test:testCloneableV2SuccessLiteralIsAcceptedNamespaced(bytes32,bytes) (runs: 2048, μ: 431751, ~: 422707) +ICloneableV2Test:testCloneableV2SuccessLiteralIsAcceptedOpenSalt(bytes32,bytes) (runs: 2048, μ: 432314, ~: 423256) +ICloneableV2Test:testCloneableV2SuccessNearMissIsRejected(bytes32) (runs: 2048, μ: 166262, ~: 166262) +ICloneableV2Test:testCloneableV2SuccessNearMissIsWellFormed() (gas: 273) +ICloneableV2Test:testCloneableV2SuccessPinned() (gas: 267) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297955) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEip7702Designator(address,address) (runs: 2048, μ: 7422, ~: 7422) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7962, ~: 7960) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7169, ~: 7223) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 459061, ~: 443058) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 466856, ~: 450600) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 552856, ~: 537248) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 560902, ~: 544857) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 467536, ~: 451203) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 456513, ~: 438428) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 359037, ~: 349969) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 561665, ~: 545415) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 550479, ~: 532662) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEmptyData(bytes32,bytes) (runs: 2048, μ: 385199, ~: 385184) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 430819, ~: 421885) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltExtremeSalts(bytes) (runs: 2048, μ: 552758, ~: 535431) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltFactoryScoped(bytes32,bytes) (runs: 2048, μ: 995361, ~: 977453) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltImplementationIsByAddress(bytes32,bytes) (runs: 2048, μ: 855549, ~: 837641) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167225, ~: 167225) 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) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltLargeData(bytes32,bytes1) (runs: 2048, μ: 6360129, ~: 8726306) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549352, ~: 533117) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433513, ~: 424488) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltOrderIndependent(bytes32,bytes32,bytes) (runs: 2048, μ: 798425, ~: 765827) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12291, ~: 12269) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictIsStaticCallable(address,bytes,bytes32) (runs: 2048, μ: 8872, ~: 8844) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445132, ~: 1040445368) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11142, ~: 11151) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 558768, ~: 542729) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 430819, ~: 421889) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicExtremeSalts(bytes) (runs: 2048, μ: 560054, ~: 542536) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicFactoryScoped(bytes32,bytes) (runs: 2048, μ: 994234, ~: 976355) 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:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 549258, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 433026, ~: 424015) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicPredictCallerIndependent(address,bytes32,address,address,address) (runs: 2048, μ: 13695, ~: 13695) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicPredictIsStaticCallable(address,bytes32,address) (runs: 2048, μ: 7502, ~: 7502) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicPredictZeroDeployer(address,bytes32) (runs: 2048, μ: 9672, ~: 9672) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5564, ~: 5564) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445673, ~: 1040445840) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 553760, ~: 538213) 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) @@ -31,7 +50,7 @@ LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesReal 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:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4686, ~: 4682) LibICloneableFactoryV4Test:testEffectiveOpenSaltEmptyData(bytes32) (runs: 2048, μ: 826, ~: 826) LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1269, ~: 1257) LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1360, ~: 1348) diff --git a/test/src/concrete/TestCloneFactory.sol b/test/concrete/TestCloneFactory.sol similarity index 100% rename from test/src/concrete/TestCloneFactory.sol rename to test/concrete/TestCloneFactory.sol diff --git a/test/concrete/TestCloneable.sol b/test/concrete/TestCloneable.sol new file mode 100644 index 0000000..1d9cc0f --- /dev/null +++ b/test/concrete/TestCloneable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// Thrown by a second call to `TestCloneable.initialize`. `ICloneableV2` says +/// the implementation MUST ensure `initialize` can NOT be called more than +/// once; this is how this fixture ensures it. +error TestCloneableAlreadyInitialized(); + +/// @title TestCloneable +/// @notice THE conforming `ICloneableV2` fixture. Every test that needs a +/// clone that initializes successfully uses this one, so there is a single +/// place where "what a correct `ICloneableV2` does" is written down, and every +/// flow test in the suite is run against something that actually honours the +/// interface rather than against the minimum the factory happens to check. +/// +/// Three properties, each load bearing: +/// +/// - It stores whatever `data` it was initialized with in the public `sData`, +/// so a test can prove the bytes reached the clone verbatim. +/// - `initialize` can NOT be called more than once — the interface's first +/// normative MUST. The flag is written before the data so a re-entrant call +/// cannot slip past the guard. +/// - It returns the success sentinel written out from the LITERAL STRING +/// `ICloneableV2` names, NOT the imported `ICLONEABLE_V2_SUCCESS`. Importing +/// the constant would put both sides of the library's comparison in +/// lockstep: change the constant and every clone still initializes, because +/// the fixture changed with it. A third party implementing `ICloneableV2` +/// has no such luxury — the interface tells them to return +/// `keccak256("ICloneableV2.initialize")` and they hard-code that value — so +/// the fixture hard-codes it too, and a drift in the constant surfaces as a +/// real `InitializationFailed` through a real factory. +/// +/// It also carries the RECOMMENDED typed overload, which the interface +/// requires to revert `InitializeSignatureFn` always. +contract TestCloneable is ICloneableV2 { + /// The data this clone was initialized with. Set once. + bytes public sData; + + /// Whether `initialize` has already run on this clone. Storage lives on + /// the clone, not the implementation, because the factory reaches this + /// code through an EIP-1167 `DELEGATECALL` proxy. + bool public sInitialized; + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + if (sInitialized) { + revert TestCloneableAlreadyInitialized(); + } + sInitialized = true; + sData = data; + // Deliberately the literal, not `ICLONEABLE_V2_SUCCESS`. See the + // contract notice. + return keccak256("ICloneableV2.initialize"); + } + + /// The RECOMMENDED typed overload of `initialize`, which exists only so an + /// initialization config type appears in the ABI. `ICloneableV2` requires + /// it to revert `InitializeSignatureFn` ALWAYS, so that it is never + /// accidentally called in place of the generic `initialize(bytes)` the + /// factory calls. The parameter is unnamed because it is never read. + /// @return Never returns; the declared return type only exists so the + /// overload has the shape a real typed `initialize` would. + function initialize(uint256) external pure returns (bytes32) { + revert InitializeSignatureFn(); + } +} diff --git a/test/src/concrete/TestCloneableFailure.sol b/test/concrete/TestCloneableFailure.sol similarity index 100% rename from test/src/concrete/TestCloneableFailure.sol rename to test/concrete/TestCloneableFailure.sol diff --git a/test/src/concrete/TestCloneable.sol b/test/src/concrete/TestCloneable.sol deleted file mode 100644 index 6fcf0a7..0000000 --- a/test/src/concrete/TestCloneable.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; - -/// @title 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/interface/ICloneableV2.t.sol b/test/src/interface/ICloneableV2.t.sol new file mode 100644 index 0000000..03b2ab9 --- /dev/null +++ b/test/src/interface/ICloneableV2.t.sol @@ -0,0 +1,115 @@ +// 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_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 {InitializationFailed} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; + +/// @title ICloneableV2Test +/// @notice Pins `ICLONEABLE_V2_SUCCESS`, the initialization success sentinel. +/// +/// The sentinel is a value THIRD PARTIES REPRODUCE. `ICloneableV2` tells an +/// implementer to "return the keccak256 hash of the string +/// `ICloneableV2.initialize`", so every `ICloneableV2` in the world hard-codes +/// that hash, and a factory that compares against anything else rejects all of +/// them. It is therefore consensus-critical in exactly the way the two factory +/// domain tags are, and is pinned the same way `testDomainTagsPinned` pins +/// those: recomputed from the literal string, never read back from the +/// constant. +/// +/// The end-to-end tests here are what stop the constant drifting undetected. +/// `TestCloneable` hard-codes the literal hash rather than importing +/// `ICLONEABLE_V2_SUCCESS` precisely so that it stands in for an independent +/// third-party implementation: were it to import the constant the library +/// compares against, both sides of that comparison would move together and the +/// whole suite would stay green under a changed sentinel. These tests say so +/// deliberately, on both derivations, rather than leaving it to be an +/// accidental property of the flow tests. +contract ICloneableV2Test is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The sentinel is exactly the pinned string hash. Recomputed from the + /// literal so this is independent of the constant, and of anything that + /// imports it. + function testCloneableV2SuccessPinned() external pure { + assertEq(ICLONEABLE_V2_SUCCESS, keccak256("ICloneableV2.initialize")); + } + + /// The sentinel is its own value, distinct from the two factory domain + /// tags: three separate string-derived constants that must never be + /// conflated. + function testCloneableV2SuccessDistinctFromDomainTags() external pure { + assertTrue(ICLONEABLE_V2_SUCCESS != ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN); + assertTrue(ICLONEABLE_V2_SUCCESS != ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN); + } + + /// THE CONTRACT WITH THIRD PARTIES, namespaced path. An implementation + /// that returns the LITERAL hash — not the imported constant — initializes + /// successfully through a real factory, and the child really holds the + /// data it was initialized with. If the sentinel the library accepts ever + /// stopped being `keccak256("ICloneableV2.initialize")`, every independent + /// `ICloneableV2` would start reverting `InitializationFailed` here. + function testCloneableV2SuccessLiteralIsAcceptedNamespaced(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(TestCloneable(child).sData(), data); + } + + /// The same contract with third parties, open-salt path. + function testCloneableV2SuccessLiteralIsAcceptedOpenSalt(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(TestCloneable(child).sData(), data); + } + + /// The comparison is over the EXACT hash, not over "returned a plausible + /// 32-byte word". `TestCloneableFailure` returns whatever `bytes32` its + /// initialization data decodes to, so handing it the hash of a string ONE + /// CHARACTER away from the one the spec names produces exactly the near + /// miss: a well-formed 32-byte return that reaches the comparison rather + /// than reverting earlier. It is rejected, and the address is left free + /// rather than occupied by an uninitialized clone. + function testCloneableV2SuccessNearMissIsRejected(bytes32 salt) external { + TestCloneableFailure implementation = new TestCloneableFailure(); + bytes memory nearMissData = abi.encode(keccak256("ICloneableV2.initialise")); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), nearMissData, salt); + + assertEq(predicted.code.length, 0); + } + + /// The near miss really is a near miss and not some degenerate value: it + /// is a nonzero word that differs from the sentinel. Stated here so the + /// rejection above cannot be passing for an uninteresting reason. + function testCloneableV2SuccessNearMissIsWellFormed() external pure { + bytes32 nearMiss = keccak256("ICloneableV2.initialise"); + assertTrue(nearMiss != bytes32(0)); + assertTrue(nearMiss != ICLONEABLE_V2_SUCCESS); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol index 07f8ce4..52b3d00 100644 --- a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -5,7 +5,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibICloneableFactoryV4, ZeroImplementationCodeSize} from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; /// @title LibICloneableFactoryV4CheckImplementationCodeTest /// @notice Tests `LibICloneableFactoryV4.checkImplementationCode`: a codeless @@ -38,7 +38,55 @@ contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { vm.assume(implementation.code.length == 0); vm.assume(uint160(implementation) > 0x0a); vm.assume(code.length > 0); + // EIP-3541 forbids DEPLOYING any code whose first byte is `0xef`, so + // no CREATE or CREATE2 can put such code at an address. That leaves + // exactly one way an account can hold it — an EIP-7702 delegation + // designator, which is `0xef0100` followed by an address and is + // therefore EXACTLY 23 bytes. That case is real, so it is not excluded + // here, it is pinned by its own test below. + // + // What this exclusion drops is the rest: `0xef`-leading blobs of any + // other length, which no chain can produce. It cannot weaken the + // property under test, because the guard only ever looks at code + // LENGTH. + // + // It is also what keeps this test from failing for a harness reason: + // `vm.etch` parses a `0xef01` prefix as an EIP-7702 delegation + // designator and rejects it unless the blob is exactly 23 bytes + // ("Eip7702 is not 23 bytes long"), so a fuzz run that drew one died + // in the cheatcode rather than in the code under test. + vm.assume(code[0] != 0xef); vm.etch(implementation, code); LibICloneableFactoryV4.checkImplementationCode(implementation); } + + /// The one `0xef`-leading code a real account can hold: an EIP-7702 + /// delegation designator, `0xef0100 || address`, exactly 23 bytes. The + /// fuzz test above cannot reach it, so it is pinned here as a fixed case. + /// + /// It PASSES the guard, and that is the point worth having on the record. + /// `EXTCODESIZE` on a delegated EOA returns 23, not zero, so the size + /// check cannot tell an ordinary implementation contract from an EOA that + /// has delegated — and unlike a deployed contract, a delegation is + /// REVOCABLE by the account holder at any time. A caller who wants an + /// immutable implementation does not get that from this guard; the guard + /// promises only that something is there. + /// + /// Scoped honestly: `foundry.toml` pins `evm_version = "cancun"`, which + /// predates EIP-7702, so what is asserted here is that the 23-byte + /// designator is storable at an address and passes the SIZE check. The + /// execution semantics of delegation are not exercised and this test does + /// not claim them. + function testCheckImplementationCodeEip7702Designator(address delegated, address delegate) external { + vm.assume(delegated.code.length == 0); + vm.assume(uint160(delegated) > 0x0a); + + bytes memory designator = abi.encodePacked(hex"ef0100", delegate); + assertEq(designator.length, 23, "an EIP-7702 designator is 23 bytes"); + + vm.etch(delegated, designator); + + assertEq(delegated.code.length, 23, "EXTCODESIZE sees the designator, not zero"); + LibICloneableFactoryV4.checkImplementationCode(delegated); + } } diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol index 8d82b5f..4808519 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -12,9 +12,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministic` / @@ -180,4 +180,114 @@ contract LibICloneableFactoryV4CloneDeterministicTest is Test { vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); I_CLONE_FACTORY.cloneDeterministic(implementation, data, salt); } + + /// The PREDICTION does not read the caller. `predictDeterministicAddress` + /// takes the deployer as a parameter precisely so that anyone can predict + /// on anyone's behalf, so the same `(implementation, salt, deployer)` asked + /// from two different accounts must give the same answer. This is the + /// namespaced mirror of + /// `testCloneDeterministicOpenSaltPredictCallerIndependent`: without it the + /// only thing pinning `deployer` against `msg.sender` is that + /// `…SaltIsDomainTaggedHash` happens to fuzz `deployer` from one fixed + /// caller. + function testCloneDeterministicPredictCallerIndependent( + address implementation, + bytes32 salt, + address deployer, + address alice, + address bob + ) external { + vm.assume(alice != bob); + + vm.prank(alice); + address predictedFromAlice = I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer); + + vm.prank(bob); + address predictedFromBob = I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer); + + assertEq(predictedFromAlice, predictedFromBob); + + // And the answer is the deployer's, not either caller's: asking about + // `alice` gives a different address than asking about `bob`, no matter + // who asks. + vm.assume(deployer != alice); + vm.prank(bob); + assertTrue(I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, alice) != predictedFromBob); + } + + /// `deployer` is an arbitrary account identifier, not a live caller, so + /// `address(0)` is a perfectly well-defined input and gets the pinned + /// formula like any other. A prediction that quietly substituted + /// `msg.sender` for a zero deployer would be a plausible "helpful default" + /// and is ruled out here. + function testCloneDeterministicPredictZeroDeployer(address implementation, bytes32 salt) external view { + bytes32 effectiveSalt = keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, address(0), salt)); + address expected = Clones.predictDeterministicAddress(implementation, effectiveSalt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, address(0)), expected); + + // And it is not the caller's answer wearing a disguise. + assertTrue( + I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, address(0)) + != I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, address(this)) + ); + } + + /// The FACTORY is in the address too — `address(this)`, read inside the + /// library. Two factories, everything else held equal, are two different + /// addresses, and each really deploys at its own. A pinned clone address + /// is only meaningful against a named factory. + function testCloneDeterministicFactoryScoped(bytes32 salt, bytes memory data) external { + TestCloneFactory otherFactory = new TestCloneFactory(); + TestCloneable implementation = new TestCloneable(); + + address predictedHere = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + address predictedThere = otherFactory.predictDeterministicAddress(address(implementation), salt, address(this)); + assertTrue(predictedHere != predictedThere); + + assertEq(I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt), predictedHere); + assertEq(otherFactory.cloneDeterministic(address(implementation), data, salt), predictedThere); + } + + /// The extremes of the salt space are ordinary salts. `bytes32(0)` and + /// `type(uint256).max` both predict, both deploy where predicted, and are + /// distinct from each other — the salt goes into a `keccak256` preimage, so + /// there is no edge to fall off, and this states that rather than leaving it + /// to a fuzzer that may never pick either. + function testCloneDeterministicExtremeSalts(bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predictedZero = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), bytes32(0), address(this)); + address predictedMax = I_CLONE_FACTORY.predictDeterministicAddress( + address(implementation), bytes32(type(uint256).max), address(this) + ); + assertTrue(predictedZero != predictedMax); + + address childZero = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, bytes32(0)); + address childMax = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, bytes32(type(uint256).max)); + + assertEq(childZero, predictedZero); + assertEq(childMax, predictedMax); + assertEq(TestCloneable(childZero).sData(), data); + assertEq(TestCloneable(childMax).sData(), data); + } + + /// The prediction writes no state: called through a raw `STATICCALL` it + /// still answers, and answers the same thing the typed call does. The + /// library function is `view` and the compiler enforces that on the + /// delegating concrete, but nothing else in the suite exercises the + /// prediction at the EVM boundary where a state write would actually + /// revert. + function testCloneDeterministicPredictIsStaticCallable(address implementation, bytes32 salt, address deployer) + external + view + { + (bool ok, bytes memory ret) = address(I_CLONE_FACTORY) + .staticcall(abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddress, (implementation, salt, deployer))); + assertTrue(ok); + assertEq( + abi.decode(ret, (address)), I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer) + ); + } } diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol index 1e59618..2fa1e45 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -16,9 +16,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministicOpenSalt` / @@ -371,4 +371,151 @@ contract LibICloneableFactoryV4CloneDeterministicOpenSaltTest is Test { vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); I_CLONE_FACTORY.cloneDeterministicOpenSalt(implementation, data, salt); } + + /// The FACTORY is in the address even though the caller is not. Two + /// factories, same `(implementation, data, salt)`, are two different + /// addresses and each really deploys at its own. This is the exact limit of + /// "every account reaches the same address": every account reaches the same + /// address ON A GIVEN FACTORY, and the cross-network note on + /// `ICloneableFactoryV4` turns on precisely this — the factory has to be at + /// the same address on both chains. + function testCloneDeterministicOpenSaltFactoryScoped(bytes32 salt, bytes memory data) external { + TestCloneFactory otherFactory = new TestCloneFactory(); + TestCloneable implementation = new TestCloneable(); + + address predictedHere = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + address predictedThere = otherFactory.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + assertTrue(predictedHere != predictedThere); + + assertEq(I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt), predictedHere); + assertEq(otherFactory.cloneDeterministicOpenSalt(address(implementation), data, salt), predictedThere); + } + + /// The IMPLEMENTATION is in the address, and by its ADDRESS rather than by + /// its code: two deploys of identical bytecode are two different + /// implementations as far as the derivation is concerned. This is the + /// mechanism behind the cross-network note — an implementation deployed by + /// an ordinary nonce-dependent `CREATE` on each chain lands at a different + /// address on each, and so does every clone of it. + function testCloneDeterministicOpenSaltImplementationIsByAddress(bytes32 salt, bytes memory data) external { + TestCloneable implementationA = new TestCloneable(); + TestCloneable implementationB = new TestCloneable(); + + assertEq(address(implementationA).code, address(implementationB).code); + assertTrue(address(implementationA) != address(implementationB)); + + address predictedA = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementationA), data, salt); + address predictedB = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementationB), data, salt); + assertTrue(predictedA != predictedB); + + assertEq(I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementationA), data, salt), predictedA); + assertEq(I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementationB), data, salt), predictedB); + } + + /// Empty `data` is explicitly supported — `ICloneableFactoryV4` says so + /// twice, and the registry-resolved shape it describes is expected to pass + /// nothing. End to end: it predicts, it deploys where predicted, it + /// initializes to empty, and it is a DIFFERENT address from any non-empty + /// data at the same salt, because `keccak256("")` is just another word in + /// the preimage. + function testCloneDeterministicOpenSaltEmptyData(bytes32 salt, bytes memory data) external { + vm.assume(data.length > 0); + TestCloneable implementation = new TestCloneable(); + + address predictedEmpty = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), "", salt); + address childEmpty = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), "", salt); + + assertEq(childEmpty, predictedEmpty); + assertEq(TestCloneable(childEmpty).sData(), ""); + assertEq(TestCloneable(childEmpty).sData().length, 0); + + assertTrue( + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt) != predictedEmpty + ); + } + + /// `data` enters the derivation BY HASH, so there is no length at which the + /// preimage stops being 96 bytes and no length at which the derivation + /// changes shape. Ten kilobytes of it behaves exactly like four bytes. + function testCloneDeterministicOpenSaltLargeData(bytes32 salt, bytes1 fill) external { + TestCloneable implementation = new TestCloneable(); + + bytes memory data = new bytes(10_000); + for (uint256 i = 0; i < data.length; ++i) { + data[i] = fill; + } + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(child, predicted); + assertEq(TestCloneable(child).sData(), data); + + // One byte of difference anywhere in ten kilobytes is a different + // address: the commitment is to the whole of `data`, not a prefix. + data[9_999] = ~fill; + assertTrue( + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt) != predicted + ); + } + + /// ORDER INDEPENDENCE. Two open-salt deploys land at the same two addresses + /// whichever order they happen in, and neither consumes the other's + /// address. The factory holds no state — no nonce, no counter — so nothing + /// about a deploy can depend on what was deployed before it. State is + /// snapshotted and rolled back so both orderings genuinely start from the + /// same state. + function testCloneDeterministicOpenSaltOrderIndependent(bytes32 saltA, bytes32 saltB, bytes memory data) external { + vm.assume(saltA != saltB); + TestCloneable implementation = new TestCloneable(); + + uint256 snapshot = vm.snapshotState(); + + address firstA = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, saltA); + address firstB = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, saltB); + + vm.revertToState(snapshot); + + address secondB = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, saltB); + address secondA = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, saltA); + + assertEq(firstA, secondA); + assertEq(firstB, secondB); + assertTrue(firstA != firstB); + } + + /// The prediction writes no state: called through a raw `STATICCALL` it + /// still answers, and answers the same thing the typed call does. + function testCloneDeterministicOpenSaltPredictIsStaticCallable( + address implementation, + bytes memory data, + bytes32 salt + ) external view { + (bool ok, bytes memory ret) = address(I_CLONE_FACTORY) + .staticcall( + abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt, (implementation, data, salt)) + ); + assertTrue(ok); + assertEq( + abi.decode(ret, (address)), I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt) + ); + } + + /// The extremes of the salt space are ordinary salts on this path too. + function testCloneDeterministicOpenSaltExtremeSalts(bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predictedZero = + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, bytes32(0)); + address predictedMax = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt( + address(implementation), data, bytes32(type(uint256).max) + ); + assertTrue(predictedZero != predictedMax); + + assertEq(I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, bytes32(0)), predictedZero); + assertEq( + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, bytes32(type(uint256).max)), + predictedMax + ); + } }