diff --git a/.gas-snapshot b/.gas-snapshot index 8c76c86..687ae49 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,42 +1,71 @@ -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) -LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) +ICloneableFactoryV1DeclarationTest:testICloneableFactoryV1AbiPinned() (gas: 10518) +ICloneableFactoryV2DeclarationTest:testAbiPinned() (gas: 10466) +ICloneableFactoryV2DeclarationTest:testCloneSelectorPinned() (gas: 290) +ICloneableFactoryV2DeclarationTest:testNewCloneTopicRelationshipsAcrossTheFamily() (gas: 270) +ICloneableFactoryV2DeclarationTest:testNewCloneTopicZeroPinned() (gas: 267) +ICloneableFactoryV3DeclarationTest:testFunctionAbiPinned() (gas: 15955) +ICloneableFactoryV3DeclarationTest:testFunctionSelectorsPinned() (gas: 325) +ICloneableFactoryV3DeclarationTest:testNewCloneAbiPinned() (gas: 13998) +ICloneableFactoryV3DeclarationTest:testNewCloneNameIsSharedAcrossTheInterfaceFamily() (gas: 313) +ICloneableFactoryV3DeclarationTest:testNewCloneSharedByBothEntryPoints(bytes32,bytes) (runs: 2048, μ: 1001856, ~: 980796) +ICloneableFactoryV3DeclarationTest:testNewCloneTopicZeroPinned() (gas: 245) +ICloneableV1DeclarationTest:testICloneableV1AbiPinned() (gas: 8110) +ICloneableV2InitializeTest:testAbiPinned() (gas: 11499) +ICloneableV2InitializeTest:testInitializeOnlyOnce(bytes32,bytes,bytes) (runs: 2048, μ: 436859, ~: 426854) +ICloneableV2InitializeTest:testInitializeRevertBubblesVerbatim(bytes32,bytes) (runs: 2048, μ: 186098, ~: 185874) +ICloneableV2InitializeTest:testInitializeSelectorPinned() (gas: 245) +ICloneableV2InitializeTest:testInitializeSelectorSharedWithV1() (gas: 267) +ICloneableV2InitializeTest:testInitializeSignatureFnSelectorPinned() (gas: 290) +ICloneableV2InitializeTest:testNothingCalledBeforeInitialize(bytes32,bytes) (runs: 2048, μ: 514544, ~: 503844) +ICloneableV2InitializeTest:testSuccessSentinelPinned() (gas: 289) +ICloneableV2InitializeTest:testTypedOverloadRevertsInitializeSignatureFn(bytes32,bytes,uint256) (runs: 2048, μ: 431755, ~: 421429) +ICloneableV2InitializeTest:testV1ShapedImplementationIsRejected(bytes32,bytes) (runs: 2048, μ: 352503, ~: 342027) +IFactoryDeclarationTest:testIFactoryAbiPinned() (gas: 14451) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 297955) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEip7702Designator(address,address) (runs: 2048, μ: 7422, ~: 7422) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7963, ~: 7960) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7169, ~: 7223) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 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:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 556515, ~: 537270) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 563780, ~: 544860) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8141, ~: 8126) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 564025, ~: 545415) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 553592, ~: 532640) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 432410, ~: 421907) LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) -LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 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:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6501, ~: 6482) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 553403, ~: 533074) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 435093, ~: 424488) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12248, ~: 12224) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040445177, ~: 1040445384) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11113, ~: 11129) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 561623, ~: 542710) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 432388, ~: 421889) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455229, ~: 438823) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361175, ~: 352030) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 553402, ~: 533035) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 434604, ~: 424015) LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040443447, ~: 1040443612) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 459986, ~: 444001) -LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11086, ~: 11111) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040445709, ~: 1040445875) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 557614, ~: 538213) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11088, ~: 11111) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) -LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1405, ~: 1398) +LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1406, ~: 1398) LibICloneableFactoryV4Test:testDomainTagsDistinct() (gas: 233) LibICloneableFactoryV4Test:testDomainTagsPinned() (gas: 325) -LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4687, ~: 4682) +LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4688, ~: 4682) LibICloneableFactoryV4Test:testEffectiveOpenSaltEmptyData(bytes32) (runs: 2048, μ: 826, ~: 826) -LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1269, ~: 1257) -LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1360, ~: 1348) -LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4177, ~: 4166) +LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1270, ~: 1257) +LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1361, ~: 1348) +LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4179, ~: 4166) LibICloneableFactoryV4Test:testEffectiveSaltDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) LibICloneableFactoryV4Test:testEffectiveSaltMatchesFormula(address,bytes32) (runs: 2048, μ: 865, ~: 865) LibICloneableFactoryV4Test:testEffectiveSaltPreimageShape(address,bytes32) (runs: 2048, μ: 966, ~: 966) -LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) \ No newline at end of file +LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) +TestCloneFactoryTest:testCloneEntryPointsRouteToTheirOwnDerivation(bytes32,bytes) (runs: 2048, μ: 553292, ~: 532270) +TestCloneFactoryTest:testImplementsICloneableFactoryV4() (gas: 306) +TestCloneFactoryTest:testPredictDeterministicAddressIsPureDelegation(address,bytes32,address) (runs: 2048, μ: 6132, ~: 6132) +TestCloneFactoryTest:testPredictDeterministicAddressOpenSaltIsPureDelegation(address,bytes,bytes32) (runs: 2048, μ: 7039, ~: 7020) +TestCloneFactoryTest:testPredictionsAreStatic(address,bytes,bytes32,address) (runs: 2048, μ: 13621, ~: 13590) \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index ce13e37..d32f561 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,14 @@ cbor_metadata = false libs = ["dependencies"] +# The interface half of this repo is a PUBLISHED ABI: downstream indexers and +# soldeer consumers decode `NewClone` and call `initialize` by NAME, from the +# compiled ABI. Parameter names, parameter order among same-typed parameters, +# `indexed` flags and return types are all invisible to the EVM but visible to +# those consumers, so the only place they can be pinned by a test is the +# compiled artifact. Read-only access to `out` exists for exactly that. +fs_permissions = [{ access = "read", path = "./out" }] + [fuzz] runs = 2048 diff --git a/test/src/concrete/TestCloneFactory.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/TestCloneFactory.t.sol b/test/concrete/TestCloneFactory.t.sol new file mode 100644 index 0000000..e7fa1c4 --- /dev/null +++ b/test/concrete/TestCloneFactory.t.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; + +/// @title TestCloneFactoryTest +/// @notice `TestCloneFactory` is the executable stand-in for the real +/// `CloneFactory` in rain.factory.deploy, and the claim it exists to prove is +/// STRUCTURAL: the library surface suffices for a concrete that adds no +/// behaviour of its own. The 22 flow tests use it as a means to reach the +/// library; nothing asserted the delegation property itself, so a concrete +/// that quietly added behaviour — or that transposed two same-typed arguments +/// on the way through — would still have looked fine. +/// +/// This pins it: each of the four entry points equals the library function it +/// claims to be, computed independently here, and the two predictions really +/// are `view`. +contract TestCloneFactoryTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The concrete is an `ICloneableFactoryV4`, and therefore also carries + /// `ICloneableFactoryV3`'s two functions. Asserted as a real cast rather + /// than left to the inheritance list. + function testImplementsICloneableFactoryV4() external view { + ICloneableFactoryV4 factory = ICloneableFactoryV4(address(I_CLONE_FACTORY)); + assertEq(address(factory), address(I_CLONE_FACTORY)); + } + + /// `predictDeterministicAddress` is the library function, argument for + /// argument. `implementation` and `deployer` are both `address` and sit in + /// different positions, so transposing them would compile silently; fuzzing + /// them independently is what makes this discriminating. + function testPredictDeterministicAddressIsPureDelegation(address implementation, bytes32 salt, address deployer) + external + view + { + assertEq( + I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer), + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), implementation, LibICloneableFactoryV4.effectiveSalt(deployer, salt) + ) + ); + } + + /// `predictDeterministicAddressOpenSalt` is the library function, argument + /// for argument. + function testPredictDeterministicAddressOpenSaltIsPureDelegation( + address implementation, + bytes memory data, + bytes32 salt + ) external view { + assertEq( + I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt), + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), implementation, LibICloneableFactoryV4.effectiveOpenSalt(salt, data) + ) + ); + } + + /// Both predictions are `external view` as the interface declares: a + /// `staticcall` to each succeeds and returns the same answer the ordinary + /// call does. A concrete that widened either to `nonpayable` — adding + /// behaviour the interface forbids — would fail the staticcall. + function testPredictionsAreStatic(address implementation, bytes memory data, bytes32 salt, address deployer) + external + view + { + (bool okNamespaced, bytes memory namespaced) = address(I_CLONE_FACTORY) + .staticcall(abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddress, (implementation, salt, deployer))); + assertTrue(okNamespaced, "predictDeterministicAddress is not static"); + assertEq( + abi.decode(namespaced, (address)), + I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer) + ); + + (bool okOpen, bytes memory open) = address(I_CLONE_FACTORY) + .staticcall( + abi.encodeCall(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt, (implementation, data, salt)) + ); + assertTrue(okOpen, "predictDeterministicAddressOpenSalt is not static"); + assertEq( + abi.decode(open, (address)), I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt) + ); + } + + /// The two deploying entry points route to the two DIFFERENT library + /// derivations, each landing where that derivation says and nowhere else. + /// Together with the prediction tests this covers all four delegations and + /// pins that none of them is wired to the other's derivation. + function testCloneEntryPointsRouteToTheirOwnDerivation(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address namespaced = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + address open = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq( + namespaced, + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), + address(implementation), + LibICloneableFactoryV4.effectiveSalt(address(this), salt) + ) + ); + assertEq( + open, + LibICloneableFactoryV4.predictCloneAddress( + address(I_CLONE_FACTORY), address(implementation), LibICloneableFactoryV4.effectiveOpenSalt(salt, data) + ) + ); + } +} diff --git a/test/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/concrete/TestCloneableCallRecorder.sol b/test/concrete/TestCloneableCallRecorder.sol new file mode 100644 index 0000000..39d580d --- /dev/null +++ b/test/concrete/TestCloneableCallRecorder.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableCallRecorder +/// @notice An `ICloneableV2` that records the selector of every call the proxy +/// receives, in order, and emits a log of its own from inside `initialize`. +/// +/// `TestCloneable` exposes only the END STATE of a clone, so two clauses of +/// the shared factory spec are invisible through it: "MUST call `initialize` +/// ... MUST NOT call any other functions on the cloned proxy before +/// `initialize` completes successfully", and the ordering of the factory's +/// `NewClone` against the clone's own initialization. This fixture makes both +/// observable — `selectors()` is the whole call sequence, and `Initializing` +/// lands in the log stream at the moment `initialize` runs. +contract TestCloneableCallRecorder is ICloneableV2 { + /// Emitted from inside `initialize`, so the log stream orders the + /// factory's `NewClone` against the initialization call itself. + /// @param data The initialization data as the clone received it. + event Initializing(bytes data); + + /// Every selector the proxy has been called with, in order. Storage lives + /// on the clone, not the implementation, because the factory reaches this + /// code through an EIP-1167 `DELEGATECALL` proxy. + bytes4[] internal sSelectors; + + /// The data this clone was initialized with. + bytes public sData; + + /// The selectors recorded so far, in call order. + /// @return The recorded selectors. + function selectors() external view returns (bytes4[] memory) { + return sSelectors; + } + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + sSelectors.push(msg.sig); + sData = data; + emit Initializing(data); + // Deliberately the literal rather than `ICLONEABLE_V2_SUCCESS`, for + // the same reason `TestCloneable` writes it out: a fixture that + // imports the constant the library compares against moves in lockstep + // with it and cannot discriminate a drift. + return keccak256("ICloneableV2.initialize"); + } + + /// Records any other call the proxy receives, so a call the factory makes + /// before `initialize` cannot go unseen. Deliberately does not revert: a + /// factory that ignored the result of a stray call would otherwise leave + /// no trace at all. + fallback() external { + sSelectors.push(msg.sig); + } +} diff --git a/test/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/concrete/TestCloneableRevert.sol b/test/concrete/TestCloneableRevert.sol new file mode 100644 index 0000000..af08396 --- /dev/null +++ b/test/concrete/TestCloneableRevert.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// Thrown unconditionally by `TestCloneableRevert.initialize`, carrying the +/// data it was called with so a test can prove both that the revert came from +/// the implementation rather than the factory, and that its arguments survive +/// the trip verbatim. +/// @param data The initialization data the clone was called with. +error TestCloneableRevertInitialize(bytes data); + +/// @title TestCloneableRevert +/// @notice An `ICloneableV2` whose `initialize` REVERTS, with its own typed +/// error carrying its own data. +/// +/// This is the other half of "initialization failed" from +/// `TestCloneableFailure`, and the two are not interchangeable. +/// `TestCloneableFailure` RETURNS a non-success value, which is the case the +/// library answers with its own `InitializationFailed`. This one REFUSES — +/// what a real implementation does when its `data` does not decode or its +/// invariants do not hold — and the library must let that revert through +/// untouched instead of flattening it. A fixture that returns cannot exercise +/// that, and a fixture that reverts cannot exercise the sentinel comparison, +/// so both exist. +contract TestCloneableRevert is ICloneableV2 { + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external pure returns (bytes32) { + revert TestCloneableRevertInitialize(data); + } +} diff --git a/test/concrete/TestCloneableV1Shaped.sol b/test/concrete/TestCloneableV1Shaped.sol new file mode 100644 index 0000000..e02432e --- /dev/null +++ b/test/concrete/TestCloneableV1Shaped.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; + +/// @title TestCloneableV1Shaped +/// @notice A legacy `ICloneableV1`: `initialize(bytes)` with NO return value. +/// Its selector is `initialize(bytes)` — byte for byte the same selector +/// `ICloneableV2` publishes — so a V4 factory will happily call it and only +/// the return arity distinguishes the two interfaces. The fixture exists so +/// that difference is exercised rather than assumed. +contract TestCloneableV1Shaped is ICloneableV1 { + /// Set by `initialize`, so a test can prove the call actually landed. + bytes public sData; + + /// @inheritdoc ICloneableV1 + function initialize(bytes memory data) external { + sData = data; + } +} diff --git a/test/lib/LibPublishedAbi.sol b/test/lib/LibPublishedAbi.sol new file mode 100644 index 0000000..f27b764 --- /dev/null +++ b/test/lib/LibPublishedAbi.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Vm} from "forge-std-1.16.1/src/Vm.sol"; + +/// @dev The `Vm` address, as forge-std computes it. Duplicated here rather than +/// inherited from `Test` so this helper is usable from a plain library. +Vm constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + +/// @title LibPublishedAbi +/// @notice Reads a compiled artifact's `abi` array so a test can pin what the +/// EVM cannot see. +/// +/// Everything in `src/interface` is a PUBLISHED surface: downstream indexers +/// and soldeer consumers decode `NewClone` and call `initialize` from the ABI, +/// BY NAME. Four things in a declaration are consumer-visible but completely +/// invisible on chain: +/// +/// - parameter NAMES, +/// - the ORDER of parameters that share a type (swapping two `address` event +/// parameters changes no topic, no log data byte and no selector), +/// - `indexed` flags on parameters that are not currently indexed, +/// - RETURN types (they are not part of a function selector). +/// +/// A log-level or selector-level assertion cannot discriminate any of them, so +/// the compiled artifact is the only oracle. `forge test` recompiles before it +/// runs, so the artifact read here is never stale with respect to the source +/// under test. +library LibPublishedAbi { + /// The full artifact JSON for `.sol/.json` under `out`. + /// @param file The solidity file name, without extension. + /// @param name The contract or interface name within it. + /// @return The artifact JSON. + function artifactJson(string memory file, string memory name) internal view returns (string memory) { + return VM.readFile(string.concat("out/", file, ".sol/", name, ".json")); + } +} 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/ICloneableFactoryV2.t.sol b/test/src/interface/ICloneableFactoryV2.t.sol new file mode 100644 index 0000000..37e40cd --- /dev/null +++ b/test/src/interface/ICloneableFactoryV2.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title ICloneableFactoryV2DeclarationTest +/// @notice `ICloneableFactoryV2` is the legacy, nonce-dependent factory +/// interface. Nothing in `src/` or `test/` imports it and it has no logic, but +/// it is PUBLISHED: it ships in the soldeer package and consumers pinned to it +/// compile against these exact bytes. Declaration-only is not the same as +/// unowned, so the surface is pinned here rather than left to drift silently. +contract ICloneableFactoryV2DeclarationTest is Test { + /// The legacy `clone` selector. + function testCloneSelectorPinned() external pure { + assertEq(ICloneableFactoryV2.clone.selector, bytes4(keccak256("clone(address,bytes)"))); + } + + /// The legacy `NewClone` topic. + function testNewCloneTopicZeroPinned() external pure { + assertEq(ICloneableFactoryV2.NewClone.selector, keccak256("NewClone(address,address,address)")); + } + + /// A PUBLISHED-SURFACE HAZARD, pinned as a fact rather than asserted away. + /// Three of the four factory interfaces in this package declare an event + /// called `NewClone`. `ICloneableFactoryV1` and `ICloneableFactoryV2` + /// declare the SAME three-parameter signature, so they are literally the + /// same topic and an indexer cannot tell a V1 clone from a V2 clone by + /// `topics[0]`. `ICloneableFactoryV3` takes five parameters and is a + /// different topic entirely. Any change to these relationships is a + /// breaking change for every downstream indexer. + function testNewCloneTopicRelationshipsAcrossTheFamily() external pure { + assertEq(ICloneableFactoryV1.NewClone.selector, ICloneableFactoryV2.NewClone.selector); + assertTrue(ICloneableFactoryV2.NewClone.selector != ICloneableFactoryV3.NewClone.selector); + } + + /// The declaration itself, as published: three unindexed `address` + /// parameters in the order sender, implementation, clone; and + /// `clone(address implementation, bytes data)` returning an unnamed + /// `address`. + function testAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV2", "ICloneableFactoryV2"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "NewClone: sender, implementation, clone - in that order, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "clone(implementation, data) returns address" + ); + } +} diff --git a/test/src/interface/ICloneableFactoryV3.t.sol b/test/src/interface/ICloneableFactoryV3.t.sol new file mode 100644 index 0000000..3f5a81e --- /dev/null +++ b/test/src/interface/ICloneableFactoryV3.t.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; + +/// @title ICloneableFactoryV3DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableFactoryV3` — the +/// `NewClone` event and the two function signatures — as a consumer sees it. +/// +/// The flow tests already assert `topics[0]` and the whole log data blob, and +/// that is enough for anything the EVM can see. It is NOT enough for the +/// event's actual contract, which is that "an indexer can reconstruct the +/// deploy from the event alone": an indexer decodes the five fields BY NAME +/// from the ABI. Swapping two of the three `address` parameters in the +/// declaration leaves every topic and every data byte identical while telling +/// every consumer that the deployer is the clone — so it is the artifact, not +/// the log, that has to be pinned. +contract ICloneableFactoryV3DeclarationTest is Test { + /// `NewClone`'s wire identity: the five-parameter signature, hashed. This + /// is what an indexer subscribes to, and it is deliberately restated from + /// the literal string rather than read back off the event. + function testNewCloneTopicZeroPinned() external pure { + assertEq(ICloneableFactoryV3.NewClone.selector, keccak256("NewClone(address,address,address,bytes32,bytes)")); + } + + /// `ICloneableFactoryV3.NewClone` and `ICloneableFactoryV2.NewClone` share + /// a NAME across the published interface family and are DIFFERENT events: + /// five parameters against three, and therefore different `topics[0]`. + /// Pinned as a fact of the published surface — an indexer keyed on + /// `topics[0]` separates them, one keyed on the name alone does not. + function testNewCloneNameIsSharedAcrossTheInterfaceFamily() external pure { + assertTrue(ICloneableFactoryV3.NewClone.selector != ICloneableFactoryV2.NewClone.selector); + assertEq(ICloneableFactoryV2.NewClone.selector, keccak256("NewClone(address,address,address)")); + } + + /// The two function selectors of the V3 surface. + function testFunctionSelectorsPinned() external pure { + assertEq( + ICloneableFactoryV3.cloneDeterministic.selector, + bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)")) + ); + assertEq( + ICloneableFactoryV3.predictDeterministicAddress.selector, + bytes4(keccak256("predictDeterministicAddress(address,bytes32,address)")) + ); + } + + /// THE DECLARATION ITSELF. The five parameters in order, each with its + /// published name and type, none of them `indexed`, and the event not + /// anonymous. Written out as the ABI a consumer downloads, so a reorder or + /// a rename — neither of which any log assertion can see — fails here. + function testNewCloneAbiPinned() external view { + assertTrue( + vm.contains( + LibPublishedAbi.artifactJson("ICloneableFactoryV3", "ICloneableFactoryV3"), + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"indexed\":false,\"internalType\":\"bytes32\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}" + "],\"anonymous\":false}" + ), + "NewClone: sender, implementation, clone, salt, data - in that order, none indexed" + ); + } + + /// The V3 function declarations, as published. + function testFunctionAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV3", "ICloneableFactoryV3"); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"cloneDeterministic\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "cloneDeterministic(implementation, data, salt) returns address" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"predictDeterministicAddress\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"salt\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}," + "{\"name\":\"deployer\",\"type\":\"address\",\"internalType\":\"address\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"view\"}" + ), + "predictDeterministicAddress(implementation, salt, deployer) returns address, view" + ); + } + + /// `NewClone` is SHARED IDENTICALLY by both entry points, not merely + /// emitted by each: the same `topics[0]`, the same topic count, and the + /// same data layout come out of `cloneDeterministic` and + /// `cloneDeterministicOpenSalt` for the same caller, implementation, salt + /// and data. Asserted against each other in one test, so a change that + /// forked the two would fail here even if it kept each entry point + /// self-consistent. + function testNewCloneSharedByBothEntryPoints(bytes32 salt, bytes memory data) external { + TestCloneFactory factory = new TestCloneFactory(); + TestCloneable implementation = new TestCloneable(); + + uint256 snapshot = vm.snapshotState(); + + vm.recordLogs(); + address namespacedChild = factory.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory namespacedLogs = vm.getRecordedLogs(); + + vm.revertToState(snapshot); + + vm.recordLogs(); + address openChild = factory.cloneDeterministicOpenSalt(address(implementation), data, salt); + Vm.Log[] memory openLogs = vm.getRecordedLogs(); + + assertEq(namespacedLogs.length, 1); + assertEq(openLogs.length, 1); + assertEq(namespacedLogs[0].topics.length, 1); + assertEq(openLogs[0].topics.length, 1); + assertEq(namespacedLogs[0].topics[0], openLogs[0].topics[0]); + assertEq(namespacedLogs[0].topics[0], ICloneableFactoryV3.NewClone.selector); + + // The two derivations put the clone at different addresses, so the + // blobs differ in exactly that one field and nowhere else. + assertTrue(namespacedChild != openChild); + assertEq( + namespacedLogs[0].data, abi.encode(address(this), address(implementation), namespacedChild, salt, data) + ); + assertEq(openLogs[0].data, abi.encode(address(this), address(implementation), openChild, salt, data)); + } +} diff --git a/test/src/interface/ICloneableV2.initialize.t.sol b/test/src/interface/ICloneableV2.initialize.t.sol new file mode 100644 index 0000000..24bc15b --- /dev/null +++ b/test/src/interface/ICloneableV2.initialize.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable, TestCloneableAlreadyInitialized} from "test/concrete/TestCloneable.sol"; +import {TestCloneableRevert, TestCloneableRevertInitialize} from "test/concrete/TestCloneableRevert.sol"; +import {TestCloneableCallRecorder} from "test/concrete/TestCloneableCallRecorder.sol"; +import {TestCloneableV1Shaped} from "test/concrete/TestCloneableV1Shaped.sol"; + +/// @title ICloneableV2InitializeTest +/// @notice Pins the PUBLISHED declaration of `ICloneableV2.initialize` and +/// makes the interface's two normative MUSTs executable. +/// +/// `initialize`'s selector is compile-guarded — retype or rename it and the +/// library stops compiling — but `InitializeSignatureFn` is not: nothing in +/// this repo referenced it before this file, so its selector could drift +/// freely while every test stayed green. It is the error every conforming +/// implementation is required to revert with, so its selector is exactly the +/// kind of value a downstream consumer decodes. +contract ICloneableV2InitializeTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The generic entry point's selector — what `cloneAndInitialize` calls. + function testInitializeSelectorPinned() external pure { + assertEq(ICloneableV2.initialize.selector, bytes4(keccak256("initialize(bytes)"))); + } + + /// `ICloneableV1.initialize` and `ICloneableV2.initialize` publish the + /// SAME selector and differ only in return arity — the whole of the V1/V2 + /// difference, and the reason a V4 factory cannot tell one from the other + /// before calling it. + function testInitializeSelectorSharedWithV1() external pure { + assertEq(ICloneableV1.initialize.selector, ICloneableV2.initialize.selector); + } + + /// The error every conforming implementation's typed overload must revert + /// with, pinned from its literal signature. + function testInitializeSignatureFnSelectorPinned() external pure { + assertEq(ICloneableV2.InitializeSignatureFn.selector, bytes4(keccak256("InitializeSignatureFn()"))); + } + + /// The success sentinel is the hash of the documented string. + function testSuccessSentinelPinned() external pure { + assertEq(ICLONEABLE_V2_SUCCESS, keccak256("ICloneableV2.initialize")); + } + + /// THE DECLARATION ITSELF: `initialize(bytes data)` returning a named + /// `bytes32 success`, and a zero-parameter `InitializeSignatureFn` error. + /// Return types and parameter names are outside the selector, so this is + /// the only place they are pinned. + function testAbiPinned() external view { + string memory json = LibPublishedAbi.artifactJson("ICloneableV2", "ICloneableV2"); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"success\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "initialize(bytes data) returns (bytes32 success)" + ); + assertTrue( + vm.contains(json, "{\"type\":\"error\",\"name\":\"InitializeSignatureFn\",\"inputs\":[]}"), + "error InitializeSignatureFn() takes no parameters" + ); + } + + /// MUST: `initialize` can NOT be called more than once. Exercised end to + /// end on a clone the factory just produced, so the guard is proven where + /// it matters — after the factory's own atomic initialization has already + /// consumed the one permitted call. The stored data is unchanged by the + /// rejected second call. + function testInitializeOnlyOnce(bytes32 salt, bytes memory data, bytes memory otherData) external { + TestCloneable implementation = new TestCloneable(); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(TestCloneable(child).sData(), data); + + vm.expectRevert(abi.encodeWithSelector(TestCloneableAlreadyInitialized.selector)); + TestCloneable(child).initialize(otherData); + + assertEq(TestCloneable(child).sData(), data); + } + + /// MUST: a typed overload of `initialize` reverts `InitializeSignatureFn` + /// always, so it is never accidentally called in place of the generic + /// `initialize(bytes)` the factory calls. Asserted on a clone, uninitialized + /// and initialized alike — "always" means both. + function testTypedOverloadRevertsInitializeSignatureFn(bytes32 salt, bytes memory data, uint256 value) external { + TestCloneable implementation = new TestCloneable(); + + vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); + implementation.initialize(value); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(ICloneableV2.InitializeSignatureFn.selector)); + TestCloneable(child).initialize(value); + } + + /// A revert inside `initialize` reaches the caller VERBATIM — the + /// implementation's own typed error with its own data — rather than being + /// flattened into the library's `InitializationFailed`, which is reserved + /// for the case where `initialize` RETURNS the wrong sentinel. Nothing is + /// deployed at the address either way. + function testInitializeRevertBubblesVerbatim(bytes32 salt, bytes memory data) external { + TestCloneableRevert implementation = new TestCloneableRevert(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(abi.encodeWithSelector(TestCloneableRevertInitialize.selector, data)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } + + /// The factory calls `initialize(bytes)` and NOTHING ELSE on the fresh + /// proxy, and `NewClone` is the FIRST log of the deploy. + /// + /// Two separate oracles, because the fixture carries two. The recorded + /// SELECTOR SEQUENCE is the direct evidence for the MUST NOT: exactly one + /// call reached the clone and it was `initialize(bytes)`, so a stray call + /// cannot hide in a log stream that happens to look right. The LOG STREAM + /// is what orders `NewClone` against the clone's own initialization, which + /// no amount of end-state inspection can show. + function testNothingCalledBeforeInitialize(bytes32 salt, bytes memory data) external { + TestCloneableCallRecorder implementation = new TestCloneableCallRecorder(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + bytes4[] memory selectors = TestCloneableCallRecorder(child).selectors(); + assertEq(selectors.length, 1, "exactly one call reached the clone"); + assertEq(selectors[0], ICloneableV2.initialize.selector, "and it was initialize(bytes)"); + + assertEq(entries.length, 2, "exactly NewClone and the clone's own Initializing"); + + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], keccak256("NewClone(address,address,address,bytes32,bytes)")); + + assertEq(entries[1].emitter, child, "the second log is the clone's own"); + assertEq(entries[1].topics[0], keccak256("Initializing(bytes)")); + assertEq(entries[1].data, abi.encode(data), "initialize received the caller's data verbatim"); + } + + /// The V1/V2 selector collision above is only half a hazard statement. A + /// V4 factory calls `initialize(bytes)` on whatever address it is handed, + /// and an `ICloneableV1` answers that selector — so the question the + /// collision raises is whether a legacy implementation gets SILENTLY + /// accepted, leaving a live clone that was never really initialized. + /// + /// It does not. `initialize` returns nothing on V1, so the factory's + /// decode of a `bytes32` return finds an empty returndata buffer and + /// reverts before it ever reaches the sentinel comparison. The revert + /// carries NO data — it is the ABI decoder, not a typed error — which is + /// asserted here as the actual observed behaviour rather than assumed to + /// be `InitializationFailed`. Nothing is deployed at the address. + function testV1ShapedImplementationIsRejected(bytes32 salt, bytes memory data) external { + TestCloneableV1Shaped implementation = new TestCloneableV1Shaped(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(bytes("")); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } +} diff --git a/test/src/interface/deprecated/ICloneableFactoryV1.t.sol b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol new file mode 100644 index 0000000..3a50ef5 --- /dev/null +++ b/test/src/interface/deprecated/ICloneableFactoryV1.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableFactoryV1} from "src/interface/deprecated/ICloneableFactoryV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title ICloneableFactoryV1DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableFactoryV1`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract ICloneableFactoryV1DeclarationTest is Test { + /// `ICloneableFactoryV1`: the three-parameter `NewClone` and `clone`. + function testICloneableFactoryV1AbiPinned() external view { + assertEq(ICloneableFactoryV1.NewClone.selector, keccak256("NewClone(address,address,address)")); + assertEq(ICloneableFactoryV1.clone.selector, bytes4(keccak256("clone(address,bytes)"))); + + string memory json = LibPublishedAbi.artifactJson("ICloneableFactoryV1", "ICloneableFactoryV1"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewClone\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"clone\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "ICloneableFactoryV1.NewClone: sender, implementation, clone, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"clone\",\"inputs\":[" + "{\"name\":\"implementation\",\"type\":\"address\",\"internalType\":\"address\"}," + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableFactoryV1.clone(implementation, data) returns address" + ); + } +} diff --git a/test/src/interface/deprecated/ICloneableV1.t.sol b/test/src/interface/deprecated/ICloneableV1.t.sol new file mode 100644 index 0000000..d76dd2f --- /dev/null +++ b/test/src/interface/deprecated/ICloneableV1.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {ICloneableV1} from "src/interface/deprecated/ICloneableV1.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title ICloneableV1DeclarationTest +/// @notice Pins the PUBLISHED declaration of `ICloneableV1`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract ICloneableV1DeclarationTest is Test { + /// `ICloneableV1.initialize` has NO return value. That is the entire + /// V1/V2 difference, and it is invisible in the selector, so the ABI is + /// the only place it can be pinned. + function testICloneableV1AbiPinned() external view { + assertEq(ICloneableV1.initialize.selector, bytes4(keccak256("initialize(bytes)"))); + assertTrue( + vm.contains( + LibPublishedAbi.artifactJson("ICloneableV1", "ICloneableV1"), + "{\"type\":\"function\",\"name\":\"initialize\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[],\"stateMutability\":\"nonpayable\"}" + ), + "ICloneableV1.initialize(bytes) returns NOTHING" + ); + } +} diff --git a/test/src/interface/deprecated/IFactory.t.sol b/test/src/interface/deprecated/IFactory.t.sol new file mode 100644 index 0000000..f184d4e --- /dev/null +++ b/test/src/interface/deprecated/IFactory.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {IFactory} from "src/interface/deprecated/IFactory.sol"; +import {LibPublishedAbi} from "test/lib/LibPublishedAbi.sol"; + +/// @title IFactoryDeclarationTest +/// @notice Pins the PUBLISHED declaration of `IFactory`. +/// +/// `src/interface/deprecated/` is imported by nothing in this repo and carries +/// no logic, but it SHIPS in the soldeer package: contracts deployed years ago +/// are still described by these ABIs and indexers still decode against them. +/// Deprecated means "do not use for new work", not "free to change" — a +/// deprecated declaration that drifts silently breaks consumers that cannot be +/// redeployed. +contract IFactoryDeclarationTest is Test { + /// `IFactory`: the two events and the two functions. `isChild` returning + /// `bool` is load-bearing — the interface calls it CRITICAL to the + /// security guarantees of any implementation — and a return type is not + /// part of a selector, so only the ABI pins it. + function testIFactoryAbiPinned() external view { + assertEq(IFactory.NewChild.selector, keccak256("NewChild(address,address)")); + assertEq(IFactory.Implementation.selector, keccak256("Implementation(address,address)")); + assertEq(IFactory.createChild.selector, bytes4(keccak256("createChild(bytes)"))); + assertEq(IFactory.isChild.selector, bytes4(keccak256("isChild(address)"))); + + string memory json = LibPublishedAbi.artifactJson("IFactory", "IFactory"); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"NewChild\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"child\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.NewChild: sender, child, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"event\",\"name\":\"Implementation\",\"inputs\":[" + "{\"name\":\"sender\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}," + "{\"name\":\"implementation\",\"type\":\"address\",\"indexed\":false,\"internalType\":\"address\"}" + "],\"anonymous\":false}" + ), + "IFactory.Implementation: sender, implementation, none indexed" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"createChild\",\"inputs\":[" + "{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}]," + "\"stateMutability\":\"nonpayable\"}" + ), + "IFactory.createChild(data) returns address" + ); + assertTrue( + vm.contains( + json, + "{\"type\":\"function\",\"name\":\"isChild\",\"inputs\":[" + "{\"name\":\"maybeChild\",\"type\":\"address\",\"internalType\":\"address\"}" + "],\"outputs\":[{\"name\":\"\",\"type\":\"bool\",\"internalType\":\"bool\"}]," + "\"stateMutability\":\"view\"}" + ), + "IFactory.isChild(maybeChild) returns bool, view" + ); + } +} diff --git a/test/src/lib/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..5a8e399 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -12,9 +12,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministic` / diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol index 1e59618..558637e 100644 --- a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -16,9 +16,9 @@ import { InitializationFailed, ZeroImplementationCodeSize } from "src/lib/LibICloneableFactoryV4.sol"; -import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; -import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; -import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; +import {TestCloneFactory} from "test/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/concrete/TestCloneableFailure.sol"; /// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest /// @notice Tests `LibICloneableFactoryV4.cloneDeterministicOpenSalt` /