From 9855f9049e74bfaac891a97bd9352e587115b21b Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 20 Aug 2026 14:37:41 +0000 Subject: [PATCH 1/2] Add LibICloneableFactoryV4 salt-derivation library Extract the two ICloneableFactoryV4 effective-CREATE2-salt derivations (namespaced and open-salt) into a pure internal library, importing the domain tags from the interface so the interface and the library cannot drift. Add discriminating + fuzz tests, and update CLAUDE.md/README to record that this repo holds the derivation library and its tests. Closes #57 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 42 +++++++------- README.md | 14 ++++- src/lib/LibICloneableFactoryV4.sol | 45 +++++++++++++++ test/src/lib/LibICloneableFactoryV4.t.sol | 70 +++++++++++++++++++++++ 4 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 src/lib/LibICloneableFactoryV4.sol create mode 100644 test/src/lib/LibICloneableFactoryV4.t.sol diff --git a/CLAUDE.md b/CLAUDE.md index 7d53675..38278db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,11 @@ code in this repository. ## Project Overview -rain.factory is a Solidity **library** repo: the `ICloneable*` interface surface -for EIP1167 minimal proxy (clone) factories in the Rain ecosystem. It is the -library half of the library/deploy split (rainlanguage/rain.factory#46) and -holds interfaces only — no concrete contract, no deploy pins, no deploy script, -and no tests. +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. The concrete `CloneFactory` that implements these interfaces, its deployed address + codehash pins (`LibCloneFactoryDeploy`), the frozen @@ -26,9 +26,9 @@ SPDX headers. Nix + Foundry. Enter the shell with `nix develop`, then run rainix tasks: `rainix-sol-static` (Slither), `rainix-sol-legal` (REUSE), `rainix-sol-prelude` -(deps, run first), `rainix-sol-test`, and `forge build` directly. There is no -test suite here — the interfaces have no behaviour; the tests that exercise them -live against the concrete in rain.factory.deploy. +(deps, run first), `rainix-sol-test`, and `forge build` directly. +`rainix-sol-test` runs the derivation library's tests, which live here; the +interfaces have no behaviour to test. ## Architecture @@ -51,24 +51,26 @@ live against the concrete in rain.factory.deploy. 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/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. -`src/` holds nothing else. The interfaces import nothing from outside this repo -— intra-repo inheritance is allowed and `ICloneableFactoryV4` extends -`ICloneableFactoryV3` — which is what makes this half a standalone publish. +Nothing here imports from outside this repo — intra-repo inheritance is allowed +(`ICloneableFactoryV4` extends `ICloneableFactoryV3`), which is what makes this +half a standalone publish. ## Solidity Conventions -- Solidity version: every file here is an interface and floats `^` (the - interfaces use `^0.8.18`) so downstream soldeer consumers on a different - `0.8.x` can still compile them. The `=0.8.25` exact-pin rule applies to - concrete contracts, scripts and tests, which live in rain.factory.deploy. +- 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. - Compiler (`foundry.toml`): Cancun EVM, optimizer at 100,000 runs, no CBOR metadata (`cbor_metadata = false`, `bytecode_hash = "none"`). - Dependencies are managed with Soldeer (`[dependencies]` in `foundry.toml` + - `soldeer.lock`, vendored under `dependencies/`). The interfaces import nothing - external, so the only entry is forge-std. `@openzeppelin-contracts`, - `rain-extrospection`, `rain-deploy` and `rain-sol-codegen` belong to the - deploy half and must not be added here: adding one means concrete code has - landed in a library repo. + `soldeer.lock`, vendored under `dependencies/`). The only external entry is + forge-std. `@openzeppelin-contracts`, `rain-extrospection`, `rain-deploy` and + `rain-sol-codegen` belong to the deploy half and must not be added here: + adding one means concrete code has landed in a library repo. diff --git a/README.md b/README.md index e9ef73f..0ccacb1 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 nothing else. It publishes to Soldeer as -`rain-factory`. +`ICloneable*` interface surface and the `LibICloneableFactoryV4` salt-derivation +library. It publishes to Soldeer as `rain-factory`. ## Concrete implementations @@ -21,6 +21,16 @@ Depend on `rain-factory` if you need only the interfaces. 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 interface and +the library cannot drift, and a factory, an indexer or a consumer predicting a +clone address computes the salt from one place. Its tests live here under +`test/src/lib/`. + ## Interfaces Contains interfaces for working with Rain factories. diff --git a/src/lib/LibICloneableFactoryV4.sol b/src/lib/LibICloneableFactoryV4.sol new file mode 100644 index 0000000..2de05c0 --- /dev/null +++ b/src/lib/LibICloneableFactoryV4.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.18; + +import { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "../interface/ICloneableFactoryV4.sol"; + +/// @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. +library LibICloneableFactoryV4 { + /// The effective `CREATE2` salt for the namespaced derivation + /// (`cloneDeterministic` / `predictDeterministicAddress`): the caller-chosen + /// `salt` behind the namespaced domain tag and `deployer`, so the salt — and + /// therefore the clone address — is namespaced to the account that deploys. + /// @param deployer The account whose namespace the salt belongs to (the + /// `msg.sender` of `cloneDeterministic`, or the `deployer` argument of + /// `predictDeterministicAddress`). + /// @param salt The caller-chosen salt, before namespacing. + /// @return The effective salt `CREATE2` hashes. + function effectiveSalt(address deployer, bytes32 salt) internal pure returns (bytes32) { + return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)); + } + + /// The effective `CREATE2` salt for the open-salt derivation + /// (`cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`): + /// the caller-chosen `salt` and the hash of the initialization `data` behind + /// the open-salt domain tag, and nothing about the caller, so the clone + /// address commits to `salt` and `data` and every account reaches the same + /// one. `data` MAY be empty. + /// @param salt The caller-chosen salt. + /// @param data The initialization data passed to `ICloneableV2.initialize`. + /// @return The effective salt `CREATE2` hashes. + function effectiveOpenSalt(bytes32 salt, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))); + } +} diff --git a/test/src/lib/LibICloneableFactoryV4.t.sol b/test/src/lib/LibICloneableFactoryV4.t.sol new file mode 100644 index 0000000..e98d08a --- /dev/null +++ b/test/src/lib/LibICloneableFactoryV4.t.sol @@ -0,0 +1,70 @@ +// 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"; +import { + ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, + ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN +} from "src/interface/ICloneableFactoryV4.sol"; + +/// @title LibICloneableFactoryV4Test +/// Every expectation is recomputed inline from the pinned formula, never read +/// back from the library, so a change to the library's derivation diverges from +/// the oracle here and the test fails. +contract LibICloneableFactoryV4Test is Test { + /// The domain tags the library derives against are exactly the two pinned + /// string hashes. Recomputed from the literal strings so this is independent + /// of the imported constants. + function testDomainTagsPinned() external pure { + assertEq(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, keccak256("rain.factory.clone.namespaced")); + assertEq(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, keccak256("rain.factory.clone.opensalt")); + } + + /// `effectiveSalt` is the namespaced formula byte for byte. + function testEffectiveSaltMatchesFormula(address deployer, bytes32 salt) external pure { + assertEq( + LibICloneableFactoryV4.effectiveSalt(deployer, salt), + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, deployer, salt)) + ); + } + + /// `effectiveOpenSalt` is the open-salt formula byte for byte, across data + /// lengths. + function testEffectiveOpenSaltMatchesFormula(bytes32 salt, bytes memory data) external pure { + assertEq( + LibICloneableFactoryV4.effectiveOpenSalt(salt, data), + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))) + ); + } + + /// Empty `data` is a supported, distinct case: the derivation hashes + /// `keccak256("")`, not the empty bytes inline. + function testEffectiveOpenSaltEmptyData(bytes32 salt) external pure { + assertEq( + LibICloneableFactoryV4.effectiveOpenSalt(salt, ""), + keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(""))) + ); + } + + /// The two derivations use distinct fixed tags in word 0 — the whole of the + /// disjointness-by-construction guarantee. + function testDomainTagsDistinct() external pure { + assertTrue(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN != ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN); + } + + /// No namespaced `(deployer, salt)` input collides with any open-salt + /// `(salt, data)` input: the distinct word-0 tags make the preimages + /// disjoint, so the images are too. Fuzzed over both input spaces at once, + /// including empty and non-empty data. + function testDerivationsDisjoint(address deployer, bytes32 namespacedSalt, bytes32 openSalt, bytes memory data) + external + pure + { + assertTrue( + LibICloneableFactoryV4.effectiveSalt(deployer, namespacedSalt) + != LibICloneableFactoryV4.effectiveOpenSalt(openSalt, data) + ); + } +} From ba2764e72be5df6435d8fa667155930061b5b267 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Thu, 20 Aug 2026 14:48:23 +0000 Subject: [PATCH 2/2] Qualify doc drift/import-boundary claims (CodeRabbit) Scope CLAUDE.md's no-external-imports statement to src/ (tests import forge-std), and in README attribute one-source-of-truth to the imported tags while crediting the tests for pinning the formulas byte for byte. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 6 +++--- README.md | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 38278db..2c9468e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,9 +57,9 @@ interfaces have no behaviour to test. - `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. -Nothing here imports from outside this repo — intra-repo inheritance is allowed -(`ICloneableFactoryV4` extends `ICloneableFactoryV3`), which is what makes this -half a standalone publish. +No `src/` file imports from outside this repo — intra-repo inheritance is +allowed (`ICloneableFactoryV4` extends `ICloneableFactoryV3`), which is what +makes this half a standalone publish. ## Solidity Conventions diff --git a/README.md b/README.md index 0ccacb1..7b03e12 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,11 @@ Depend on `rain-factory` if you need only the interfaces. Depend on `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 interface and -the library cannot drift, and a factory, an indexer or a consumer predicting a -clone address computes the salt from one place. Its tests live here under -`test/src/lib/`. +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/`. ## Interfaces