diff --git a/.coderabbitai.yaml b/.coderabbitai.yaml index 63c3ccb..c6e9c57 100644 --- a/.coderabbitai.yaml +++ b/.coderabbitai.yaml @@ -1,6 +1,5 @@ # SPDX-License-Identifier: LicenseRef-DCL-1.0 # SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd - reviews: path_filters: - "!audit/**" diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..8c76c86 --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,42 @@ +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeContract() (gas: 248218) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeEtched(address,bytes) (runs: 2048, μ: 7528, ~: 7527) +LibICloneableFactoryV4CheckImplementationCodeTest:testCheckImplementationCodeZero(address) (runs: 2048, μ: 7214, ~: 7267) +LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeDeploysEIP1167Runtime(address,bytes32) (runs: 2048, μ: 45636, ~: 45636) +LibICloneableFactoryV4CloneCreationCodeTest:testCloneCreationCodeIsEIP1167(address) (runs: 2048, μ: 4245, ~: 4245) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 459061, ~: 443058) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 466856, ~: 450600) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 8139, ~: 8126) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat(address,bytes,bytes) (runs: 2048, μ: 467536, ~: 451203) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 456513, ~: 438428) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 359037, ~: 349969) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 167203, ~: 167203) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 6499, ~: 6482) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455295, ~: 438862) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361663, ~: 352503) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 12247, ~: 12224) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040442913, ~: 1040443151) +LibICloneableFactoryV4CloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11124, ~: 11129) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicDataNotInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 464734, ~: 448450) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 359015, ~: 349951) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166695, ~: 166695) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 455229, ~: 438823) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 361175, ~: 352030) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSaltIsDomainTaggedHash(address,bytes32,address) (runs: 2048, μ: 5563, ~: 5563) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSecondDeployReverts(bytes32,bytes) (runs: 2048, μ: 1040443447, ~: 1040443612) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 459986, ~: 444001) +LibICloneableFactoryV4CloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 11086, ~: 11111) +LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressIsCreate2Formula(address,address,bytes32) (runs: 2048, μ: 1724, ~: 1724) +LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesOZ(address,address,bytes32) (runs: 2048, μ: 1570, ~: 1570) +LibICloneableFactoryV4PredictCloneAddressTest:testPredictCloneAddressMatchesRealDeploy(address,bytes32) (runs: 2048, μ: 42869, ~: 42869) +LibICloneableFactoryV4Test:testDerivationsDisjoint(address,bytes32,bytes32,bytes) (runs: 2048, μ: 1405, ~: 1398) +LibICloneableFactoryV4Test:testDomainTagsDistinct() (gas: 233) +LibICloneableFactoryV4Test:testDomainTagsPinned() (gas: 325) +LibICloneableFactoryV4Test:testEffectiveOpenSaltDataSensitive(bytes32,bytes,bytes) (runs: 2048, μ: 4687, ~: 4682) +LibICloneableFactoryV4Test:testEffectiveOpenSaltEmptyData(bytes32) (runs: 2048, μ: 826, ~: 826) +LibICloneableFactoryV4Test:testEffectiveOpenSaltMatchesFormula(bytes32,bytes) (runs: 2048, μ: 1269, ~: 1257) +LibICloneableFactoryV4Test:testEffectiveOpenSaltPreimageShape(bytes32,bytes) (runs: 2048, μ: 1360, ~: 1348) +LibICloneableFactoryV4Test:testEffectiveOpenSaltSaltSensitive(bytes32,bytes32,bytes) (runs: 2048, μ: 4177, ~: 4166) +LibICloneableFactoryV4Test:testEffectiveSaltDeployerSensitive(address,address,bytes32) (runs: 2048, μ: 3858, ~: 3858) +LibICloneableFactoryV4Test:testEffectiveSaltMatchesFormula(address,bytes32) (runs: 2048, μ: 865, ~: 865) +LibICloneableFactoryV4Test:testEffectiveSaltPreimageShape(address,bytes32) (runs: 2048, μ: 966, ~: 966) +LibICloneableFactoryV4Test:testEffectiveSaltSaltSensitive(address,bytes32,bytes32) (runs: 2048, μ: 3819, ~: 3819) \ No newline at end of file diff --git a/.github/workflows/package-release.yaml b/.github/workflows/package-release.yaml index 9e7608c..41ccfd9 100644 --- a/.github/workflows/package-release.yaml +++ b/.github/workflows/package-release.yaml @@ -7,7 +7,8 @@ jobs: release: uses: rainlanguage/rainix/.github/workflows/rainix-autopublish.yaml@main with: - # Library repo: the ICloneable* interface surface. The deploy half (concrete + - # deploy-pin snapshots) lives in rain.factory.deploy (rainlanguage/rain.factory#46). + # Library repo: the ICloneable* interface surface plus the Lib* logic behind + # it. The deploy half (concrete + deploy-pin snapshots) lives in + # rain.factory.deploy (rainlanguage/rain.factory#46). soldeer-package: rain-factory secrets: inherit diff --git a/.vscode/settings.json b/.vscode/settings.json index 3905ae4..9119366 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.rulers": [80] -} \ No newline at end of file + "editor.rulers": [80] +} diff --git a/CLAUDE.md b/CLAUDE.md index 2c9468e..30b9ff9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,17 +7,17 @@ code in this repository. rain.factory is a Solidity **library** repo for EIP1167 minimal proxy (clone) factories in the Rain ecosystem: the `ICloneable*` interface surface and the -`LibICloneableFactoryV4` salt-derivation library. It is the library half of the -library/deploy split (rainlanguage/rain.factory#46) — no concrete contract, no -deploy pins, no deploy script. +`LibICloneableFactoryV4` library that implements it. It is the library half of +the library/deploy split (rainlanguage/rain.factory#46) — no concrete contract, +no deploy pins, no deploy script. -The concrete `CloneFactory` that implements these interfaces, its deployed -address + codehash pins (`LibCloneFactoryDeploy`), the frozen -`src/generated//` deploy-pin snapshots and `script/Deploy.sol` all live in -[`rain.factory.deploy`](https://github.com/rainlanguage/rain.factory.deploy) and -publish as the `rain-factory-deploy` Soldeer package. Consumers that need only -the interfaces depend on `rain-factory`; consumers that need the deployed -address/codehash pins depend on `rain-factory-deploy`. +The concrete `CloneFactory` — meant to be a pure delegation into +`LibICloneableFactoryV4`, one call per entry point — its deployed address + +codehash pins (`LibCloneFactoryDeploy`), the frozen deploy-pin snapshots and +`script/Deploy.sol` all live in +[`rain.factory.deploy`](https://github.com/rainlanguage/rain.factory.deploy), +publishing as `rain-factory-deploy`. Depend on `rain-factory` for the interfaces +and library; on `rain-factory-deploy` for the deployed pins. License: LicenseRef-DCL-1.0 (DecentraLicense). All source files must include SPDX headers. @@ -26,34 +26,33 @@ SPDX headers. Nix + Foundry. Enter the shell with `nix develop`, then run rainix tasks: `rainix-sol-static` (Slither), `rainix-sol-legal` (REUSE), `rainix-sol-prelude` -(deps, run first), `rainix-sol-test`, and `forge build` directly. -`rainix-sol-test` runs the derivation library's tests, which live here; the -interfaces have no behaviour to test. +(deps, run first), `rainix-sol-test`, and `forge build` / `forge test` directly. +Tests exercise the library through `TestCloneFactory`, a pure-delegation +concrete, with OZ `Clones` as foreign EIP1167 oracle. ## Architecture -- `src/interface/ICloneableV2.sol` — Interface for cloneable contracts. - `initialize(bytes)` must return `ICLONEABLE_V2_SUCCESS` (keccak256 hash) on - success. -- `src/interface/ICloneableFactoryV2.sol` — Legacy factory interface: the - nonce-dependent `clone(address, bytes)` and `NewClone` event. The concrete - factory in rain.factory.deploy implements `ICloneableFactoryV4`, not this; - still published for other consumers. -- `src/interface/ICloneableFactoryV3.sol` — Deterministic-only factory - interface: `cloneDeterministic(address, bytes, bytes32)` + - `predictDeterministicAddress(address, bytes32, address)` (CREATE2, salt - namespaced by `msg.sender`) and its own `NewClone` event. Standalone — does - NOT extend `ICloneableFactoryV2` and has no non-deterministic `clone()`. Still - published for consumers pinned to it. +- `src/interface/ICloneableV2.sol` — Cloneable contracts. `initialize(bytes)` + must return `ICLONEABLE_V2_SUCCESS` on success. +- `src/interface/ICloneableFactoryV2.sol` — Legacy factory interface + (nonce-dependent `clone`). Still published for other consumers. +- `src/interface/ICloneableFactoryV3.sol` — Deterministic-only factory interface + (CREATE2, salt namespaced by `msg.sender`). Standalone — does NOT extend V2; + the non-deterministic `clone()` was intentionally dropped. Still published for + consumers pinned to it. - `src/interface/ICloneableFactoryV4.sol` — Current factory interface. Extends `ICloneableFactoryV3` and defines the open-salt pair `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`. Both derivations are pinned to exact bytes, each `keccak256`-ing a 96-byte preimage led by a distinct string-derived domain tag, so the two images are disjoint by construction. The full spec is the NatSpec on the interface. -- `src/lib/LibICloneableFactoryV4.sol` — Pure `internal` library, the executable - form of the two `ICloneableFactoryV4` salt derivations; imports the domain - tags from the interface. Tested under `test/src/lib/`. +- `src/lib/LibICloneableFactoryV4.sol` — The whole factory as internal library + logic: both effective-salt derivations (pure, importing the domain tags from + the interface), the implementation-code guard, EIP1167 creation code + CREATE2 + prediction (no external cloning dependency), and the atomic + clone-initialize-verify flow with the typed errors and `NewClone`. + `msg.sender` and `address(this)` are read inside the library, so a delegating + concrete cannot misroute them. - `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. @@ -63,14 +62,15 @@ makes this half a standalone publish. ## Solidity Conventions -- Solidity version: the interfaces and the derivation library float `^` - (`^0.8.18`) so downstream soldeer consumers on a different `0.8.x` can still - compile them; the tests here pin `=0.8.25`, as do the concrete contracts and - scripts in rain.factory.deploy. +- Versions: interfaces float `^0.8.18` so downstream soldeer consumers on a + different `0.8.x` still compile them; the library floats `^0.8.25`; tests pin + `=0.8.25`, as do the concretes and scripts in rain.factory.deploy. +- No named return values. - Compiler (`foundry.toml`): Cancun EVM, optimizer at 100,000 runs, no CBOR metadata (`cbor_metadata = false`, `bytecode_hash = "none"`). - Dependencies are managed with Soldeer (`[dependencies]` in `foundry.toml` + - `soldeer.lock`, vendored under `dependencies/`). The only external entry is - forge-std. `@openzeppelin-contracts`, `rain-extrospection`, `rain-deploy` and + `soldeer.lock`, vendored under `dependencies/`). Everything there is + test-harness only: forge-std, plus `@openzeppelin-contracts` as the + equivalence oracle for the EIP1167 construction. `rain-deploy` and `rain-sol-codegen` belong to the deploy half and must not be added here: - adding one means concrete code has landed in a library repo. + needing one means deploy-pin code has landed in a library repo. diff --git a/README.md b/README.md index 7b03e12..06832d6 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Docs at https://rainprotocol.github.io/rain.factory This repo is the **library** half of the library/deploy split ([#46](https://github.com/rainlanguage/rain.factory/issues/46)): the -`ICloneable*` interface surface and the `LibICloneableFactoryV4` salt-derivation -library. It publishes to Soldeer as `rain-factory`. +`ICloneable*` interface surface and the `LibICloneableFactoryV4` library that +implements it. It publishes to Soldeer as `rain-factory`. ## Concrete implementations @@ -17,20 +17,28 @@ together with its deployed address + codehash pins, its frozen per-release deploy-pin snapshots and its deploy script. That repo publishes as `rain-factory-deploy`. -Depend on `rain-factory` if you need only the interfaces. Depend on +Depend on `rain-factory` if you need the interfaces or the library. Depend on `rain-factory-deploy` if you need the deployed address or codehash of a live `CloneFactory`. -## Derivation library - -`src/lib/LibICloneableFactoryV4.sol` is the executable form of the two `CREATE2` -salt derivations `ICloneableFactoryV4` pins to exact bytes — the -`msg.sender`-namespaced one and the open-salt one — as pure `internal` -functions. It imports the domain tags from the interface, so the tags have a -single source of truth, and its tests recompute both formulas independently to -pin them to the interface's spec byte for byte. A factory, an indexer or a -consumer predicting a clone address computes the salt from one place. The tests -live here under `test/src/lib/`. +## Library + +`src/lib/LibICloneableFactoryV4.sol` is the executable form of +`ICloneableFactoryV4` as internal library logic. The two `CREATE2` salt +derivations the interface pins to exact bytes — the `msg.sender`-namespaced one +and the open-salt one — are pure functions that import the domain tags from the +interface, so the tags have a single source of truth and a factory, an indexer +or a consumer predicting a clone address computes the salt from one place. On +top of them sit the implementation-code guard, the EIP1167 creation code and its +CREATE2 address prediction (constructed from the standard's own bytes, so the +published `src/` depends on no external cloning code), and the atomic +clone-initialize-verify flow with its typed errors and the `NewClone` event. +`msg.sender` and `address(this)` are read inside the library, so a concrete +factory is nothing but one delegation per entry point and cannot misroute +either. The tests live here under `test/src/lib/`: they recompute both salt +formulas independently to pin them to the interface's spec byte for byte, and +pin the EIP1167 construction against OpenZeppelin `Clones` as a foreign +implementation of the same standard. ## Interfaces diff --git a/REUSE.toml b/REUSE.toml index f9b7846..e85bffb 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -2,20 +2,21 @@ version = 1 [[annotations]] path = [ - ".github/workflows/**/", - ".vscode/**/", - ".coderabbitai.yaml", - ".gitignore", - "audit/**/", - "CLAUDE.md", - "README.md", - "flake.lock", - "flake.nix", - "foundry.toml", - "slither.config.json", - "REUSE.toml", - "soldeer.lock", - ".soldeerignore", + ".gas-snapshot", + ".github/workflows/**/", + ".vscode/**/", + ".coderabbitai.yaml", + ".gitignore", + "audit/**/", + "CLAUDE.md", + "README.md", + "flake.lock", + "flake.nix", + "foundry.toml", + "slither.config.json", + "REUSE.toml", + "soldeer.lock", + ".soldeerignore", ] SPDX-FileCopyrightText = "Copyright (c) 2020 Rain Open Source Software Ltd" SPDX-License-Identifier = "LicenseRef-DCL-1.0" diff --git a/foundry.toml b/foundry.toml index f3e327f..ce13e37 100644 --- a/foundry.toml +++ b/foundry.toml @@ -19,12 +19,15 @@ libs = ["dependencies"] [fuzz] runs = 2048 -# Library repo: the ICloneable* interface surface only. The concrete CloneFactory, -# its deploy-pin snapshots and deploy scripts live in rain.factory.deploy -# (rainlanguage/rain.factory#46). Interfaces import nothing, so there are no -# Solidity dependencies beyond the test harness. +# Library repo: the ICloneable* interface surface plus the Lib* logic that +# implements it. The concrete CloneFactory, its deploy-pin snapshots and deploy +# scripts live in rain.factory.deploy (rainlanguage/rain.factory#46). The +# published src/ imports nothing external, so every dependency here is +# test-harness only: forge-std, and OZ Clones as the independent equivalence +# oracle for the EIP1167 construction. [dependencies] forge-std = "1.16.1" +"@openzeppelin-contracts" = "5.6.1" [soldeer] recursive_deps = false diff --git a/slither.config.json b/slither.config.json index 522c8ae..8d9c84b 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { - "detectors_to_exclude": "assembly-usage,solc-version,pragma,unindexed-event-address", - "filter_paths": "dependencies/forge-std-1.16.1" + "detectors_to_exclude": "assembly-usage,solc-version,pragma,unindexed-event-address", + "filter_paths": "dependencies/forge-std-1.16.1" } diff --git a/soldeer.lock b/soldeer.lock index e9184e4..edfbca8 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -1,3 +1,10 @@ +[[dependencies]] +name = "@openzeppelin-contracts" +version = "5.6.1" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_6_1_15-03-2026_09:19:50_contracts.zip" +checksum = "a3b6bc661be858c7c27f60a1708cbebe8c71034b4cc1e9fe270d0a05b069352f" +integrity = "bce03af7ada1eee21a7fff393f238bcd7cd75a022a4db55ffb6b0dbb32433d35" + [[dependencies]] name = "forge-std" version = "1.16.1" diff --git a/src/lib/LibICloneableFactoryV4.sol b/src/lib/LibICloneableFactoryV4.sol index 2de05c0..d85fe9d 100644 --- a/src/lib/LibICloneableFactoryV4.sol +++ b/src/lib/LibICloneableFactoryV4.sol @@ -1,21 +1,66 @@ // SPDX-License-Identifier: LicenseRef-DCL-1.0 // SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity ^0.8.18; +pragma solidity ^0.8.25; +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "../interface/ICloneableV2.sol"; +import {ICloneableFactoryV3} from "../interface/ICloneableFactoryV3.sol"; import { ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN } from "../interface/ICloneableFactoryV4.sol"; +/// Thrown when an implementation has zero code size which is always a mistake: +/// an EIP-1167 proxy of a codeless implementation delegates every call — +/// `initialize` included — to nothing. +error ZeroImplementationCodeSize(); + +/// Thrown when the `CREATE2` deploy of the clone itself fails. With the tiny +/// fixed EIP-1167 initcode the only realistic cause is that the effective salt +/// is already taken: the exact clone asked for is already at the address, so +/// the caller can never mistake an already-initialized contract for their own +/// fresh deploy. +error CloneDeploymentFailed(); + +/// Thrown when initialization fails: `ICloneableV2.initialize` on the fresh +/// clone returned something other than `ICLONEABLE_V2_SUCCESS`. +error InitializationFailed(); + +/// @dev The EIP-1167 creation code up to the implementation address: the +/// 10-byte deploy preamble (which returns the trailing 45 bytes as runtime +/// code) followed by the first 10 bytes of the runtime. +bytes constant EIP1167_CREATION_CODE_PREFIX = hex"3d602d80600a3d3981f3363d3d373d3d3d363d73"; + +/// @dev The EIP-1167 bytes after the implementation address, shared by the +/// creation code and the runtime code. +bytes constant EIP1167_CREATION_CODE_SUFFIX = hex"5af43d82803e903d91602b57fd5bf3"; + /// @title LibICloneableFactoryV4 -/// @notice The executable form of the two effective-`CREATE2`-salt derivations -/// that `ICloneableFactoryV4` pins to exact bytes, so a factory, an indexer or a -/// consumer predicting a clone address computes them from one place instead of -/// re-deriving the formulas inline. Each function reproduces its interface -/// formula byte for byte and reads its domain tag from the interface, so the -/// tags have a single source of truth and the derivation cannot drift from the -/// spec. See `ICloneableFactoryV4` for what each salt commits to and why the two -/// images are disjoint. +/// @notice The whole of an `ICloneableFactoryV4` factory as internal library +/// logic, so a concrete factory is nothing but one delegation per entry point. +/// This is the library half of the library/deploy split +/// (rainlanguage/rain.factory#46): the derivations, guards and the +/// clone-initialize-verify flow live here, unit tested; the deploy half's +/// concrete `CloneFactory` adds no behaviour of its own. +/// +/// The library opens with the executable form of the two +/// effective-`CREATE2`-salt derivations that `ICloneableFactoryV4` pins to +/// exact bytes, so a factory, an indexer or a consumer predicting a clone +/// address computes them from one place instead of re-deriving the formulas +/// inline. Each function reproduces its interface formula byte for byte and +/// reads its domain tag from the interface, so the tags have a single source of +/// truth and the derivation cannot drift from the spec. The entry points below +/// them consume the derivations from here and nowhere else. See +/// `ICloneableFactoryV4` for what each salt commits to and why the two images +/// are disjoint — its NatSpec, with the atomic clone-and-initialize and the +/// `NewClone` event carrying the RAW caller salt, is the spec for everything +/// here. +/// +/// `msg.sender` is read INSIDE this library — `cloneDeterministic` namespaces +/// by it and `NewClone` reports it — and the internal functions execute in the +/// factory's own call context, so a delegating concrete cannot get either +/// wrong: there is no sender parameter to misroute `tx.origin` into. Likewise +/// the predictions read `address(this)`, the factory the library is inlined +/// into. library LibICloneableFactoryV4 { /// The effective `CREATE2` salt for the namespaced derivation /// (`cloneDeterministic` / `predictDeterministicAddress`): the caller-chosen @@ -42,4 +87,144 @@ library LibICloneableFactoryV4 { function effectiveOpenSalt(bytes32 salt, bytes memory data) internal pure returns (bytes32) { return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); } + + /// The canonical 55-byte EIP-1167 creation code for `implementation`. + /// Constructed from the standard's bytes directly so this library — and + /// with it the published factory — depends on no external cloning code; + /// the tests pin it byte for byte against OZ `Clones` as a foreign + /// implementation of the same standard. + /// @param implementation The contract the deployed proxy will delegate to. + /// @return The creation code. + function cloneCreationCode(address implementation) internal pure returns (bytes memory) { + return abi.encodePacked(EIP1167_CREATION_CODE_PREFIX, implementation, EIP1167_CREATION_CODE_SUFFIX); + } + + /// The address `CREATE2` gives the EIP-1167 clone of `implementation` from + /// `factory` at `derivedSalt`: + /// `address(keccak256(0xff ++ factory ++ derivedSalt ++ keccak256(creationCode)))`. + /// A pure function of its inputs, computable by anyone offchain. + /// @param factory The factory that would deploy the clone. + /// @param implementation The contract to clone. + /// @param derivedSalt The effective `CREATE2` salt, from `effectiveSalt` or + /// `effectiveOpenSalt`. + /// @return The predicted clone address. + function predictCloneAddress(address factory, address implementation, bytes32 derivedSalt) + internal + pure + returns (address) + { + return address( + uint160( + uint256( + keccak256( + abi.encodePacked(hex"ff", factory, derivedSalt, keccak256(cloneCreationCode(implementation))) + ) + ) + ) + ); + } + + /// Reverts with `ZeroImplementationCodeSize` if `implementation` has no + /// code. Always a mistake: the clone would delegate every call to nothing. + /// @param implementation The contract to clone. + function checkImplementationCode(address implementation) internal view { + if (implementation.code.length == 0) { + revert ZeroImplementationCodeSize(); + } + } + + /// The shared tail of both clone entry points: guard the implementation, + /// `CREATE2` the EIP-1167 clone at `derivedSalt`, emit `NewClone` with + /// the RAW caller salt, then run the mandatory `ICloneableV2.initialize` + /// check — atomically, with nothing else called on the proxy first, and + /// the clone only considered created if `initialize` returns + /// `ICLONEABLE_V2_SUCCESS`, per the shared spec on + /// `ICloneableFactoryV3.cloneDeterministic`. + /// @param implementation The contract to clone. + /// @param derivedSalt The effective `CREATE2` salt, from `effectiveSalt` or + /// `effectiveOpenSalt`. + /// @param data The initialization data, forwarded verbatim to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt, emitted raw in `NewClone`. + /// @return The deployed and initialized child contract address. + function cloneAndInitialize(address implementation, bytes32 derivedSalt, bytes memory data, bytes32 salt) + internal + returns (address) + { + checkImplementationCode(implementation); + bytes memory creationCode = cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), derivedSalt) + } + if (child == address(0)) { + revert CloneDeploymentFailed(); + } + emit ICloneableFactoryV3.NewClone(msg.sender, implementation, child, salt, data); + // Checking the return value of initialize is mandatory as per + // ICloneableFactoryV3 and ICloneableFactoryV4. + if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) { + revert InitializationFailed(); + } + return child; + } + + /// `ICloneableFactoryV3.cloneDeterministic`, whole: `effectiveSalt` over + /// `msg.sender` — read here, not passed, so a delegating concrete cannot + /// namespace by anything else — then the shared clone-initialize-verify + /// flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministic(address implementation, bytes memory data, bytes32 salt) internal returns (address) { + return cloneAndInitialize(implementation, effectiveSalt(msg.sender, salt), data, salt); + } + + /// `ICloneableFactoryV3.predictDeterministicAddress`, whole: the address + /// `cloneDeterministic(implementation, _, salt)` deploys to when called by + /// `deployer` on the factory this library is inlined into. + /// @param implementation The contract to clone. + /// @param salt The caller-chosen salt. + /// @param deployer The account that will call `cloneDeterministic`. + /// @return The predicted clone address. + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveSalt(deployer, salt)); + } + + /// `ICloneableFactoryV4.cloneDeterministicOpenSalt`, whole: + /// `effectiveOpenSalt` — no caller-derived value hashed in — then the + /// shared clone-initialize-verify flow. + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`, and part of the address derivation. + /// MAY be empty. + /// @param salt The caller-chosen salt. + /// @return The deployed and initialized child contract address. + function cloneDeterministicOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + returns (address) + { + return cloneAndInitialize(implementation, effectiveOpenSalt(salt, data), data, salt); + } + + /// `ICloneableFactoryV4.predictDeterministicAddressOpenSalt`, whole: the + /// address `cloneDeterministicOpenSalt(implementation, data, salt)` + /// deploys to from the factory this library is inlined into, whoever calls + /// it. + /// @param implementation The contract to clone. + /// @param data The initialization data that will be passed to + /// `ICloneableV2.initialize`. + /// @param salt The caller-chosen salt. + /// @return The predicted clone address. + function predictDeterministicAddressOpenSalt(address implementation, bytes memory data, bytes32 salt) + internal + view + returns (address) + { + return predictCloneAddress(address(this), implementation, effectiveOpenSalt(salt, data)); + } } diff --git a/test/src/concrete/TestCloneFactory.sol b/test/src/concrete/TestCloneFactory.sol new file mode 100644 index 0000000..e79231f --- /dev/null +++ b/test/src/concrete/TestCloneFactory.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +// `ICloneableFactoryV3` is imported for the `@inheritdoc` references on the +// functions it declares; `ICloneableFactoryV4` inherits rather than redeclares +// them, so the tag must name V3 and V3 must be in scope here. +import {ICloneableFactoryV3} from "src/interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV4} from "src/interface/ICloneableFactoryV4.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; + +/// @title TestCloneFactory +/// @notice A concrete `ICloneableFactoryV4` written the way the deploy half is +/// meant to write one: every function is a single delegation into +/// `LibICloneableFactoryV4` and nothing else. It exists so the flow tests exercise the +/// library through a real external surface — `msg.sender` namespacing and the +/// `NewClone` event are observable only across an external call — and it +/// doubles as the executable proof that the library surface suffices for a +/// pure-delegation concrete. +contract TestCloneFactory is ICloneableFactoryV4 { + /// @inheritdoc ICloneableFactoryV3 + function cloneDeterministic(address implementation, bytes calldata data, bytes32 salt) external returns (address) { + return LibICloneableFactoryV4.cloneDeterministic(implementation, data, salt); + } + + /// @inheritdoc ICloneableFactoryV3 + function predictDeterministicAddress(address implementation, bytes32 salt, address deployer) + external + view + returns (address) + { + return LibICloneableFactoryV4.predictDeterministicAddress(implementation, salt, deployer); + } + + /// @inheritdoc ICloneableFactoryV4 + function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + returns (address) + { + return LibICloneableFactoryV4.cloneDeterministicOpenSalt(implementation, data, salt); + } + + /// @inheritdoc ICloneableFactoryV4 + function predictDeterministicAddressOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + view + returns (address) + { + return LibICloneableFactoryV4.predictDeterministicAddressOpenSalt(implementation, data, salt); + } +} diff --git a/test/src/concrete/TestCloneable.sol b/test/src/concrete/TestCloneable.sol new file mode 100644 index 0000000..6fcf0a7 --- /dev/null +++ b/test/src/concrete/TestCloneable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneable +/// @notice A cloneable contract that implements `ICloneableV2`. Initializes +/// whatever data is passed to `initialize` as `sData`. As `sData` is public, +/// we can easily test that it is set correctly. +contract TestCloneable is ICloneableV2 { + bytes public sData; + + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external returns (bytes32) { + sData = data; + return ICLONEABLE_V2_SUCCESS; + } +} diff --git a/test/src/concrete/TestCloneableFailure.sol b/test/src/concrete/TestCloneableFailure.sol new file mode 100644 index 0000000..5088ebb --- /dev/null +++ b/test/src/concrete/TestCloneableFailure.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {ICloneableV2} from "src/interface/ICloneableV2.sol"; + +/// @title TestCloneableFailure +/// @notice A cloneable contract that implements `ICloneableV2` but always +/// fails initialization. Specifically, it returns whatever data is passed to +/// `initialize`, which is expected NOT to be `ICLONEABLE_V2_SUCCESS` for the +/// purposes of testing. +contract TestCloneableFailure is ICloneableV2 { + /// @inheritdoc ICloneableV2 + function initialize(bytes memory data) external pure returns (bytes32) { + return abi.decode(data, (bytes32)); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol new file mode 100644 index 0000000..07f8ce4 --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {LibICloneableFactoryV4, ZeroImplementationCodeSize} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; + +/// @title LibICloneableFactoryV4CheckImplementationCodeTest +/// @notice Tests `LibICloneableFactoryV4.checkImplementationCode`: a codeless +/// implementation is always a mistake — its clone would delegate every call, +/// `initialize` included, to nothing — so it reverts with a typed error +/// before any deploy happens. +contract LibICloneableFactoryV4CheckImplementationCodeTest is Test { + /// External wrapper so `vm.expectRevert` sees the internal library call at + /// its own call depth. + function checkImplementationCodeExternal(address implementation) external view { + LibICloneableFactoryV4.checkImplementationCode(implementation); + } + + /// Any address without code reverts `ZeroImplementationCodeSize`. + function testCheckImplementationCodeZero(address implementation) external { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + this.checkImplementationCodeExternal(implementation); + } + + /// A deployed contract passes. + function testCheckImplementationCodeContract() external { + TestCloneable implementation = new TestCloneable(); + LibICloneableFactoryV4.checkImplementationCode(address(implementation)); + } + + /// Any nonempty code is enough to pass: the guard is a code-size check, + /// not a validation of what the code is. + function testCheckImplementationCodeEtched(address implementation, bytes memory code) external { + vm.assume(implementation.code.length == 0); + vm.assume(uint160(implementation) > 0x0a); + vm.assume(code.length > 0); + vm.etch(implementation, code); + LibICloneableFactoryV4.checkImplementationCode(implementation); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol new file mode 100644 index 0000000..b202e39 --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.cloneCreationCode.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; + +/// @title LibICloneableFactoryV4CloneCreationCodeTest +/// @notice Tests `LibICloneableFactoryV4.cloneCreationCode` against the EIP-1167 +/// bytes written out literally here, from the EIP, so the library's constants +/// are pinned against the standard rather than against themselves. +contract LibICloneableFactoryV4CloneCreationCodeTest is Test { + /// The creation code is the canonical 55-byte EIP-1167 sequence: the + /// 10-byte deploy preamble, the 10-byte runtime prefix, the 20-byte + /// implementation address and the 15-byte runtime suffix. + function testCloneCreationCodeIsEIP1167(address implementation) external pure { + bytes memory expected = abi.encodePacked( + hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" + ); + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); + assertEq(creationCode.length, 55); + assertEq(creationCode, expected); + } + + /// Deploying the creation code really produces the EIP-1167 RUNTIME code + /// for the implementation: the same bytes minus the 10-byte deploy + /// preamble. This pins the preamble's semantics (codecopy of the trailing + /// 45 bytes) and not just its bytes. + function testCloneCreationCodeDeploysEIP1167Runtime(address implementation, bytes32 salt) external { + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), salt) + } + assertTrue(child != address(0)); + bytes memory expectedRuntime = + abi.encodePacked(hex"363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3"); + assertEq(child.code, expectedRuntime); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol new file mode 100644 index 0000000..8d82b5f --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import {ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN} from "src/interface/ICloneableFactoryV4.sol"; +import { + CloneDeploymentFailed, + InitializationFailed, + ZeroImplementationCodeSize +} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; + +/// @title LibICloneableFactoryV4CloneDeterministicTest +/// @notice Tests `LibICloneableFactoryV4.cloneDeterministic` / +/// `predictDeterministicAddress` — the namespaced pair — through +/// `TestCloneFactory`, a pure-delegation concrete, because `msg.sender` +/// namespacing and the `NewClone` event only exist across an external call. +/// The defining property is that the address commits to WHO deployed — +/// `(deployer, salt)` — and not to WHAT was initialized. +contract LibICloneableFactoryV4CloneDeterministicTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The effective `CREATE2` salt is exactly the derivation + /// `ICloneableFactoryV4` fixes: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))`. + /// Pinned against OZ's own prediction under an independently constructed + /// salt, so an off-chain caller can reproduce the address from + /// `(implementation, deployer, salt, factory)` alone and the test does not + /// restate the library's arithmetic back to itself. + function testCloneDeterministicSaltIsDomainTaggedHash(address implementation, bytes32 salt, address deployer) + external + view + { + bytes32 effectiveSalt = keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)); + address expected = Clones.predictDeterministicAddress(implementation, effectiveSalt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer), expected); + } + + /// The deployed clone lands at the predicted address, is an EIP-1167 proxy + /// of the implementation — its runtime code compared against the EIP's + /// bytes written out literally — and is initialized with the data. + /// `predict` therefore lets a caller pin the address before deploying. + function testCloneDeterministicMatchesPredict(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(child, predicted); + assertEq( + child.code, + abi.encodePacked(hex"363d3d373d3d3d363d73", address(implementation), hex"5af43d82803e903d91602b57fd5bf3") + ); + assertEq(TestCloneable(child).sData(), data); + } + + /// Distinct salts yield distinct clones of the same implementation — many + /// clones per impl (unlike a salt-free / one-per-impl deterministic + /// deploy). + function testCloneDeterministicManyClonesPerImpl(bytes32 salt1, bytes32 salt2, bytes memory data) external { + vm.assume(salt1 != salt2); + TestCloneable implementation = new TestCloneable(); + + address child1 = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt1); + address child2 = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt2); + assertTrue(child1 != child2); + } + + /// The same `(implementation, salt)` from different callers yields + /// different addresses: the salt is namespaced by `msg.sender`, so no + /// caller can squat or front-run another's address. `predict` reflects the + /// deployer. + function testCloneDeterministicSenderScoped(bytes32 salt, bytes memory data, address alice, address bob) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + address predictedAlice = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, alice); + address predictedBob = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, bob); + assertTrue(predictedAlice != predictedBob); + + vm.prank(alice); + address childAlice = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(childAlice, predictedAlice); + + vm.prank(bob); + address childBob = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + assertEq(childBob, predictedBob); + + assertTrue(childAlice != childBob); + } + + /// `data` is NOT in the derivation: the same caller and salt with two + /// different `data` land on the SAME address, each initialized with its + /// own bytes. State is snapshotted and rolled back between the two deploys + /// so both genuinely happen from the same starting state. This is the + /// namespaced pair's trade documented on `ICloneableFactoryV4`: the + /// deployer alone decides the clone's initial state at an address that + /// says nothing about it. + function testCloneDeterministicDataNotInDerivation(bytes32 salt, bytes memory dataA, bytes memory dataB) external { + vm.assume(keccak256(dataA) != keccak256(dataB)); + TestCloneable implementation = new TestCloneable(); + + uint256 snapshot = vm.snapshotState(); + + address childA = I_CLONE_FACTORY.cloneDeterministic(address(implementation), dataA, salt); + bytes memory sDataA = TestCloneable(childA).sData(); + + vm.revertToState(snapshot); + + address childB = I_CLONE_FACTORY.cloneDeterministic(address(implementation), dataB, salt); + + assertEq(childA, childB); + assertEq(sDataA, dataA); + assertEq(TestCloneable(childB).sData(), dataB); + } + + /// A second deploy at an already-taken `(deployer, salt)` reverts with the + /// library's own typed error: a caller can never mistake an + /// already-initialized contract for their own fresh deploy. + function testCloneDeterministicSecondDeployReverts(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(CloneDeploymentFailed.selector)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + // The first deploy's state is untouched by the failed second one. + assertEq(TestCloneable(child).sData(), data); + } + + /// `NewClone` is emitted with the caller, implementation, child, salt and + /// data — the full deterministic deploy, reconstructable from the event + /// alone. The salt is the RAW caller salt, not the effective one. + function testCloneDeterministicEvent(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)")))); + assertEq(entries[0].data, abi.encode(address(this), address(implementation), child, salt, data)); + } + + /// An implementation that initializes to a non-success code reverts + /// `InitializationFailed`, so clone-and-initialize stays atomic and the + /// address is left free rather than occupied by an uninitialized clone. + function testCloneDeterministicInitializeFailureFails(bytes32 notSuccess, bytes32 salt) external { + vm.assume(notSuccess != ICLONEABLE_V2_SUCCESS); + TestCloneableFailure implementation = new TestCloneableFailure(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministic(address(implementation), abi.encode(notSuccess), salt); + + assertEq(predicted.code.length, 0); + } + + /// A zero-code implementation reverts `ZeroImplementationCodeSize`. + function testCloneDeterministicZeroImplementationCodeSize(address implementation, bytes memory data, bytes32 salt) + external + { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + I_CLONE_FACTORY.cloneDeterministic(implementation, data, salt); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol new file mode 100644 index 0000000..1e59618 --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test, Vm} from "forge-std-1.16.1/src/Test.sol"; + +import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {ICLONEABLE_V2_SUCCESS} from "src/interface/ICloneableV2.sol"; +import { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "src/interface/ICloneableFactoryV4.sol"; +import { + LibICloneableFactoryV4, + CloneDeploymentFailed, + InitializationFailed, + ZeroImplementationCodeSize +} from "src/lib/LibICloneableFactoryV4.sol"; +import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol"; +import {TestCloneable} from "test/src/concrete/TestCloneable.sol"; +import {TestCloneableFailure} from "test/src/concrete/TestCloneableFailure.sol"; + +/// @title LibICloneableFactoryV4CloneDeterministicOpenSaltTest +/// @notice Tests `LibICloneableFactoryV4.cloneDeterministicOpenSalt` / +/// `predictDeterministicAddressOpenSalt` through `TestCloneFactory`, a +/// pure-delegation concrete. The defining property is that the address commits +/// to WHAT is deployed — `(implementation, data, salt)` — and to nothing about +/// WHO deploys it, which is the exact opposite of what `cloneDeterministic` +/// guarantees. So the two derivations are also tested against each other here, +/// including the one squat that the pair of distinct domain tags exists to +/// close. +contract LibICloneableFactoryV4CloneDeterministicOpenSaltTest is Test { + /// The `TestCloneFactory` instance under test. Stateless, so reused + /// everywhere. + TestCloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new TestCloneFactory(); + } + + /// The effective `CREATE2` salt is exactly the derivation + /// `ICloneableFactoryV4` fixes: + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. + /// Pinned against OZ's own prediction under an independently constructed + /// salt, so an off-chain caller can reproduce the address from + /// `(implementation, data, salt, factory)` alone and the test does not + /// restate the library's arithmetic back to itself. + function testCloneDeterministicOpenSaltIsDomainTaggedHash(address implementation, bytes memory data, bytes32 salt) + external + view + { + bytes32 effectiveSalt = keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); + address expected = Clones.predictDeterministicAddress(implementation, effectiveSalt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt), expected); + } + + /// The deployed clone lands at the predicted address, is an EIP-1167 proxy + /// of the implementation — its runtime code compared against the EIP's + /// bytes written out literally — and is initialized with the data. + /// `predict` therefore lets a caller pin the address before deploying — + /// but only once `data` is final, since `data` is in the derivation. + function testCloneDeterministicOpenSaltMatchesPredict(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(child, predicted); + assertEq( + child.code, + abi.encodePacked(hex"363d3d373d3d3d363d73", address(implementation), hex"5af43d82803e903d91602b57fd5bf3") + ); + assertEq(TestCloneable(child).sData(), data); + } + + /// THE POINT OF THIS VARIANT. The same `(implementation, data, salt)` from + /// two different callers lands on the SAME address. State is snapshotted + /// and rolled back between the two deploys so both callers genuinely + /// deploy from the same starting state — the addresses are compared, not + /// merely predicted. This is exactly what `cloneDeterministic` forbids, so + /// an address deployed here survives its original deployer being retired: + /// any other account can re-establish it on another chain. + function testCloneDeterministicOpenSaltCallerIndependent( + bytes32 salt, + bytes memory data, + address alice, + address bob + ) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + + uint256 snapshot = vm.snapshotState(); + + vm.prank(alice); + address childAlice = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + vm.revertToState(snapshot); + + vm.prank(bob); + address childBob = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(childAlice, childBob); + assertEq(childAlice, predicted); + } + + /// The prediction takes no deployer, so it cannot vary with one. + /// Predicting the same `(implementation, data, salt)` from two different + /// callers returns the same address — a caller pinning an address offchain + /// does not need to know who will deploy it. + function testCloneDeterministicOpenSaltPredictCallerIndependent( + address implementation, + bytes memory data, + bytes32 salt, + address alice, + address bob + ) external { + vm.assume(alice != bob); + + vm.prank(alice); + address predictedAlice = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt); + + vm.prank(bob); + address predictedBob = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, salt); + + assertEq(predictedAlice, predictedBob); + } + + /// `data` IS IN THE DERIVATION, which is what makes losing the + /// `msg.sender` namespacing safe. Two different `data` at the SAME + /// `(implementation, salt)` are two different addresses, and both clones + /// exist independently with their own initialization. So a front-runner + /// who passes anything other than the intended bytes deploys their own + /// contract at their own address and at their own expense, leaving the + /// address that was pinned untouched and still deployable. + function testCloneDeterministicOpenSaltDataInDerivation(bytes32 salt, bytes memory dataA, bytes memory dataB) + external + { + vm.assume(keccak256(dataA) != keccak256(dataB)); + TestCloneable implementation = new TestCloneable(); + + address predictedA = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), dataA, salt); + address predictedB = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), dataB, salt); + assertTrue(predictedA != predictedB); + + address childA = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataA, salt); + address childB = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataB, salt); + + assertEq(childA, predictedA); + assertEq(childB, predictedB); + assertEq(TestCloneable(childA).sData(), dataA); + assertEq(TestCloneable(childB).sData(), dataB); + } + + /// The two derivations are disjoint under freely varying inputs on BOTH + /// sides: no `(data, openSalt)` open-salt address is any `(namespacedSalt, + /// deployer)` sender-namespaced address. Adding the open variant therefore + /// cannot reach, block or collide with an address that `cloneDeterministic` + /// promised to a specific caller, or vice versa. + /// + /// This is the broad statement, and on its own it is weak: a collision it + /// could catch needs a keccak256 collision, so no realistic mutation of + /// the derivation makes it fail. It is kept as the plain form of the + /// interface's claim. The reachable case that actually discriminates the + /// two domain tags is the next test. + function testCloneDeterministicOpenSaltDiffersFromSenderNamespaced( + address implementation, + bytes memory data, + bytes32 openSalt, + bytes32 namespacedSalt, + address deployer + ) external view { + address open = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, data, openSalt); + address namespaced = I_CLONE_FACTORY.predictDeterministicAddress(implementation, namespacedSalt, deployer); + assertTrue(open != namespaced); + } + + /// THE SQUAT THE DISTINCT DOMAIN TAGS CLOSE, stated as an attack rather + /// than as an absence. + /// + /// `ICloneableFactoryV4` makes it a MUST NOT on the factory that no other + /// entry point can `CREATE2` in the open-salt derivation's image with + /// caller-supplied `data`. `cloneDeterministic` is precisely such an entry + /// point. Both preimages are 96 bytes and the caller chooses words 1 and 2 + /// of each: on the namespaced path `(msg.sender, salt)` and on the open + /// path `(salt, keccak256(data))`. Were the two tags ONE WORD — the same + /// tag on both derivations — an account `A` would reach EVERY open-salt + /// address whose `salt` equals `bytes32(uint256(uint160(A)))`, the + /// abi-encoding of `A`, by calling + /// `cloneDeterministic(implementation, evilData, keccak256(data))`. No + /// preimage search, just a choice of salt. Only the first word — the tag + /// no caller can set — separates the two preimages at that point. + /// + /// The premise is proven, not narrated: the attacker's namespaced words 1 + /// and 2, re-hashed under the OPEN tag, land byte for byte on the honest + /// party's open-salt effective salt. Then the guarantee: under the two + /// DISTINCT tags the real predictions differ, the attacker's deploy lands + /// at their own namespaced address, and the pinned open-salt address is + /// still free and still deploys the intended clone with the intended + /// bytes. + function testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat( + address attacker, + bytes memory data, + bytes memory evilData + ) external { + TestCloneable implementation = new TestCloneable(); + + // The open salt an honest party pinned, which happens to be the + // abi-encoding of the attacker's own address. Nothing stops a salt + // taking this value; the attacker is free to go looking for one that + // does, or to pick the address to suit the salt. + bytes32 openSalt = bytes32(uint256(uint160(attacker))); + address open = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, openSalt); + + // The attacker's namespaced salt is just `keccak256(data)`, read off + // the honest deploy they are front-running. From `attacker` the + // namespaced preimage is then + // `(NAMESPACED_DOMAIN, attacker, keccak256(data))`. + bytes32 attackerSalt = keccak256(data); + address namespaced = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), attackerSalt, attacker); + + // ATTACK PREMISE. `abi.encode` left-pads `attacker` into exactly the + // word `openSalt` already is, so the attacker's namespaced preimage + // re-tagged with the OPEN tag is byte for byte the honest open + // preimage: with one shared tag the squat lands exactly on the address + // the honest party pinned. + bytes32 sharedTagCounterfactual = + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, attacker, attackerSalt)); + assertEq( + sharedTagCounterfactual, + LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data), + "the attacker's words re-tagged ARE the open effective salt" + ); + + // And symmetrically under the NAMESPACED tag: the honest open words + // re-tagged are byte for byte the attacker's namespaced effective + // salt. Same two unknowns, solved from the other side, closed by the + // same word. + bytes32 sharedTagCounterfactualNamespaced = + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, openSalt, keccak256(data))); + assertEq( + sharedTagCounterfactualNamespaced, + LibICloneableFactoryV4.effectiveSalt(attacker, attackerSalt), + "the open words re-tagged ARE the namespaced effective salt" + ); + + // THE GUARANTEE. The two distinct tags move the real open-salt address + // off the one `cloneDeterministic` can reach. + assertTrue(open != namespaced); + + // End to end, not just in prediction: the attacker really deploys, at + // their own address, with their own data, and the honest open-salt + // address is still free afterwards and still deploys the intended + // clone with the intended bytes. + vm.prank(attacker); + address childAttacker = I_CLONE_FACTORY.cloneDeterministic(address(implementation), evilData, attackerSalt); + assertEq(childAttacker, namespaced); + assertEq(open.code.length, 0); + + address childOpen = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, openSalt); + assertEq(childOpen, open); + assertEq(TestCloneable(childOpen).sData(), data); + } + + /// REGRESSION GUARD on the guarantee that must not break, in the other + /// direction. Taking a salt via the open variant does not consume it for + /// `cloneDeterministic`: the same caller can still deploy at the same + /// `salt` through the namespaced derivation, at the address it always + /// predicted, and both clones exist independently. + function testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predictedNamespaced = + I_CLONE_FACTORY.predictDeterministicAddress(address(implementation), salt, address(this)); + + address childOpen = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + address childNamespaced = I_CLONE_FACTORY.cloneDeterministic(address(implementation), data, salt); + + assertEq(childNamespaced, predictedNamespaced); + assertTrue(childOpen != childNamespaced); + assertTrue(childOpen.code.length > 0); + assertTrue(childNamespaced.code.length > 0); + } + + /// Distinct salts yield distinct clones of the same implementation and the + /// same `data` — many clones per impl, as with the namespaced variant. The + /// other half of "distinct `(salt, data)` pairs yield distinct clones"; + /// the `data` half is `…DataInDerivation`. + function testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32 salt1, bytes32 salt2, bytes memory data) external { + vm.assume(salt1 != salt2); + TestCloneable implementation = new TestCloneable(); + + address child1 = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt1); + address child2 = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt2); + assertTrue(child1 != child2); + } + + /// A second deploy at an already-taken open salt REVERTS with the + /// library's own typed error. Since `data` is in the derivation, repeating + /// the whole `(implementation, data, salt)` is now the ONLY way to aim at + /// an address somebody else already took, and even that does not silently + /// return the existing clone: a caller can never mistake an + /// already-initialized contract for their own fresh deploy. What the + /// reverting caller would have deployed is byte-identical to what is + /// already there, so the loss is the gas and nothing else. + function testCloneDeterministicOpenSaltSecondDeployReverts( + bytes32 salt, + bytes memory data, + address alice, + address bob + ) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + vm.prank(alice); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(CloneDeploymentFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + // The first deploy's state is untouched by the failed second one. + assertEq(TestCloneable(child).sData(), data); + } + + /// `NewClone` is emitted with the caller, implementation, child, salt and + /// data. The event is shared with `cloneDeterministic` and carries the RAW + /// salt in both cases, never the effective one. `salt` and `data` together + /// are the whole of the open derivation, so the event carries enough to + /// recompute the address — an indexer picks the derivation by trying both + /// and keeping the match, which is well defined precisely because the two + /// images are disjoint. + function testCloneDeterministicOpenSaltEvent(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + vm.recordLogs(); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].emitter, address(I_CLONE_FACTORY)); + assertEq(entries[0].topics[0], bytes32(uint256(keccak256("NewClone(address,address,address,bytes32,bytes)")))); + assertEq(entries[0].data, abi.encode(address(this), address(implementation), child, salt, data)); + } + + /// An implementation that initializes to a non-success code reverts + /// `InitializationFailed`, so clone-and-initialize stays atomic and the + /// address is left free rather than occupied by an uninitialized clone. + function testCloneDeterministicOpenSaltInitializeFailureFails(bytes32 notSuccess, bytes32 salt) external { + vm.assume(notSuccess != ICLONEABLE_V2_SUCCESS); + TestCloneableFailure implementation = new TestCloneableFailure(); + + bytes memory data = abi.encode(notSuccess); + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), data, salt); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(predicted.code.length, 0); + } + + /// A zero-code implementation reverts `ZeroImplementationCodeSize`. + function testCloneDeterministicOpenSaltZeroImplementationCodeSize( + address implementation, + bytes memory data, + bytes32 salt + ) external { + vm.assume(implementation.code.length == 0); + vm.expectRevert(abi.encodeWithSelector(ZeroImplementationCodeSize.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(implementation, data, salt); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol b/test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol new file mode 100644 index 0000000..83e37e0 --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.predictCloneAddress.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; + +import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; +import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol"; + +/// @title LibICloneableFactoryV4PredictCloneAddressTest +/// @notice Tests `LibICloneableFactoryV4.predictCloneAddress` against two independent +/// oracles: OpenZeppelin's `Clones.predictDeterministicAddress` — a foreign +/// implementation of the same EIP-1167 CREATE2 prediction, so any divergence +/// in our creation-code bytes or hashing shows up as a different address — +/// and the raw CREATE2 formula computed longhand here. +contract LibICloneableFactoryV4PredictCloneAddressTest is Test { + /// Byte-for-byte equivalence with OZ Clones for every (factory, + /// implementation, effectiveSalt): same creation code, same formula, same + /// address. This is the equivalence oracle that lets the deploy half swap + /// its OZ-backed concrete for a delegation into this library. + function testPredictCloneAddressMatchesOZ(address factory, address implementation, bytes32 effectiveSalt) + external + pure + { + assertEq( + LibICloneableFactoryV4.predictCloneAddress(factory, implementation, effectiveSalt), + Clones.predictDeterministicAddress(implementation, effectiveSalt, factory) + ); + } + + /// The raw CREATE2 formula, written out longhand: + /// `address(keccak256(0xff ++ factory ++ salt ++ keccak256(creationCode)))` + /// over the EIP-1167 creation bytes written out literally. + function testPredictCloneAddressIsCreate2Formula(address factory, address implementation, bytes32 effectiveSalt) + external + pure + { + bytes memory creationCode = abi.encodePacked( + hex"3d602d80600a3d3981f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" + ); + address expected = address( + uint160(uint256(keccak256(abi.encodePacked(hex"ff", factory, effectiveSalt, keccak256(creationCode))))) + ); + assertEq(LibICloneableFactoryV4.predictCloneAddress(factory, implementation, effectiveSalt), expected); + } + + /// A real CREATE2 deploy of the creation code lands exactly where the + /// prediction says, from a live factory address (this test contract). + function testPredictCloneAddressMatchesRealDeploy(address implementation, bytes32 effectiveSalt) external { + address predicted = LibICloneableFactoryV4.predictCloneAddress(address(this), implementation, effectiveSalt); + bytes memory creationCode = LibICloneableFactoryV4.cloneCreationCode(implementation); + address child; + assembly ("memory-safe") { + child := create2(0, add(creationCode, 0x20), mload(creationCode), effectiveSalt) + } + assertEq(child, predicted); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.t.sol b/test/src/lib/LibICloneableFactoryV4.t.sol index e98d08a..06bd1c6 100644 --- a/test/src/lib/LibICloneableFactoryV4.t.sol +++ b/test/src/lib/LibICloneableFactoryV4.t.sol @@ -67,4 +67,62 @@ contract LibICloneableFactoryV4Test is Test { != LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data) ); } + + /// The namespaced preimage is 96 bytes whose FIRST word is the + /// string-derived namespaced tag. Packed independently of both the library + /// and the interface constant, so this also pins the constant to its + /// documented string. + function testEffectiveSaltPreimageShape(address deployer, bytes32 salt) external pure { + bytes memory preimage = + abi.encodePacked(keccak256("rain.factory.clone.namespaced"), bytes32(uint256(uint160(deployer))), salt); + assertEq(preimage.length, 96); + assertEq(LibICloneableFactoryV4.effectiveSalt(deployer, salt), keccak256(preimage)); + } + + /// The deployer is in the namespaced derivation: two deployers, two + /// effective salts. + function testEffectiveSaltDeployerSensitive(address alice, address bob, bytes32 salt) external pure { + vm.assume(alice != bob); + assertTrue(LibICloneableFactoryV4.effectiveSalt(alice, salt) != LibICloneableFactoryV4.effectiveSalt(bob, salt)); + } + + /// The caller salt is in the namespaced derivation: two salts, two + /// effective salts. + function testEffectiveSaltSaltSensitive(address deployer, bytes32 saltA, bytes32 saltB) external pure { + vm.assume(saltA != saltB); + assertTrue( + LibICloneableFactoryV4.effectiveSalt(deployer, saltA) + != LibICloneableFactoryV4.effectiveSalt(deployer, saltB) + ); + } + + /// The open-salt preimage is 96 bytes whose FIRST word is the + /// string-derived open-salt tag, whose second is the caller salt and whose + /// third is `keccak256(data)` — `data` enters by hash, so the preimage is + /// fixed length for any data length. Packed independently of both the + /// library and the interface constant. + function testEffectiveOpenSaltPreimageShape(bytes32 salt, bytes memory data) external pure { + bytes memory preimage = abi.encodePacked(keccak256("rain.factory.clone.opensalt"), salt, keccak256(data)); + assertEq(preimage.length, 96); + assertEq(LibICloneableFactoryV4.effectiveOpenSalt(salt, data), keccak256(preimage)); + } + + /// `data` is in the open-salt derivation: two data, two effective salts. + function testEffectiveOpenSaltDataSensitive(bytes32 salt, bytes memory dataA, bytes memory dataB) external pure { + vm.assume(keccak256(dataA) != keccak256(dataB)); + assertTrue( + LibICloneableFactoryV4.effectiveOpenSalt(salt, dataA) + != LibICloneableFactoryV4.effectiveOpenSalt(salt, dataB) + ); + } + + /// The caller salt is in the open-salt derivation: two salts, two + /// effective salts. + function testEffectiveOpenSaltSaltSensitive(bytes32 saltA, bytes32 saltB, bytes memory data) external pure { + vm.assume(saltA != saltB); + assertTrue( + LibICloneableFactoryV4.effectiveOpenSalt(saltA, data) + != LibICloneableFactoryV4.effectiveOpenSalt(saltB, data) + ); + } }