From dad2d683ba11a074011ec8fbf893200a3c0f4ebc Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 8 Aug 2026 14:20:53 +0000 Subject: [PATCH 1/7] feat: open-salt deterministic clone variant (no deployer in the derivation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `ICloneableFactoryV4`, extending `ICloneableFactoryV3` with a second deterministic entry point whose CREATE2 salt is the caller-supplied salt verbatim, so the clone address is `CREATE2(factory, salt, EIP1167(impl))` with no identity in the derivation: - `cloneDeterministicOpenSalt(address,bytes,bytes32)` - `predictDeterministicAddressOpenSalt(address,bytes32)` `cloneDeterministic` / `predictDeterministicAddress` are untouched: their `msg.sender` namespacing is a guarantee consumers rely on, so this is purely additive and the two derivations are disjoint. The open variant is only safe for implementations whose `initialize` takes no caller-controlled authority — with no sender in the salt anyone can land on the address with their own `data`, and initialization is atomic, so the first mover sets authority permanently. The NatSpec states the qualifying condition and the registry-resolved-admin pairing that satisfies it. Regenerates the 0.1.6 deploy-pin snapshot for the new bytecode. Closes #50 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 17 +- README.md | 22 +- src/concrete/CloneFactory.sol | 42 +++- src/generated/0_1_6/CloneFactory.pointers.sol | 25 ++ src/interface/ICloneableFactoryV4.sol | 130 +++++++++++ src/lib/LibCloneFactoryDeploy.sol | 2 +- ...oneFactoryCloneDeterministicOpenSalt.t.sol | 219 ++++++++++++++++++ ...LibCloneFactoryDeployTaggedConstants.t.sol | 53 +++++ 8 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 src/generated/0_1_6/CloneFactory.pointers.sol create mode 100644 src/interface/ICloneableFactoryV4.sol create mode 100644 test/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.sol diff --git a/CLAUDE.md b/CLAUDE.md index 4ceabb4..fff5012 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,15 +60,24 @@ forge build - `src/interface/ICloneableFactoryV2.sol` — Legacy factory interface: the nonce-dependent `clone(address, bytes)` and `NewClone` event. Superseded by `ICloneableFactoryV3` for `CloneFactory`; still published for other consumers. -- `src/interface/ICloneableFactoryV3.sol` — Current factory interface. - Deterministic-only: `cloneDeterministic(address, bytes, bytes32)` + +- `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`, because the non-deterministic `clone()` was intentionally dropped. +- `src/interface/ICloneableFactoryV4.sol` — Current factory interface. Extends + `ICloneableFactoryV3` (nothing was dropped, so it inherits rather than + restates) and adds the open-salt variant: + `cloneDeterministicOpenSalt(address, bytes, bytes32)` + + `predictDeterministicAddressOpenSalt(address, bytes32)`, which use the + caller-supplied salt verbatim so the deployer is not in the address + derivation. Only safe for implementations whose `initialize` takes no + caller-controlled authority — the NatSpec on the function is the spec for + that. - `src/concrete/CloneFactory.sol` — The single concrete implementation of - `ICloneableFactoryV3`. Uses OpenZeppelin `Clones.cloneDeterministic()`; there - is no plain `clone()`. + `ICloneableFactoryV4`. Uses OpenZeppelin `Clones.cloneDeterministic()` for + both variants; there is no plain `clone()`. - `src/lib/LibCloneFactoryDeploy.sol` — Deterministic deployment address and codehash constants (generated; aliases the current tag's `src/generated//` snapshot). diff --git a/README.md b/README.md index 862a228..cd21f91 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,22 @@ Docs at https://rainprotocol.github.io/rain.factory ## Concrete implementations -`CloneFactory` implements `ICloneableFactoryV2` allowing any +`CloneFactory` implements `ICloneableFactoryV4` allowing any compatible `ICloneableV2` contract to be cloned as an EIP1167 proxy and initialized. +It offers two deterministic (`CREATE2`) entry points that differ only in how the +salt is derived: + +- `cloneDeterministic` namespaces the caller-supplied salt by `msg.sender`, so + nobody else can reach the caller's address. +- `cloneDeterministicOpenSalt` uses the caller-supplied salt verbatim, so the + address is a function of `(implementation, salt)` and the factory alone — + portable across accounts and chains, but reachable by anyone. It is ONLY safe + for implementations whose `initialize` takes no caller-controlled authority; + read the NatSpec on `ICloneableFactoryV4.cloneDeterministicOpenSalt` before + using it. + ## Interfaces Contains interfaces for working with Rain factories. @@ -32,8 +44,12 @@ The onchain tooling for analysis is found at https://github.com/rainprotocol/rai The current interfaces in this repository are for -- `ICloneableFactoryV2` that is expected to clone proxies from a reference - implementation +- `ICloneableFactoryV4` that is expected to clone proxies from a reference + implementation, deterministically, with or without the deployer in the address + derivation. It extends `ICloneableFactoryV3` (deterministic-only, deployer + always in the derivation), which is still published for consumers pinned to it +- `ICloneableFactoryV2` that clones via a nonce-dependent `CREATE`. Superseded + for `CloneFactory`, still published for other consumers - A small interface `ICloneableV2` designed for cloneable proxy contracts to expose an `initialize` function that the factory can call to act like a constructor diff --git a/src/concrete/CloneFactory.sol b/src/concrete/CloneFactory.sol index be28c91..f66d8d4 100644 --- a/src/concrete/CloneFactory.sol +++ b/src/concrete/CloneFactory.sol @@ -3,7 +3,11 @@ pragma solidity =0.8.25; import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "../interface/ICloneableV2.sol"; +// `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 "../interface/ICloneableFactoryV3.sol"; +import {ICloneableFactoryV4} from "../interface/ICloneableFactoryV4.sol"; import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol"; /// Thrown when an implementation has zero code size which is always a mistake. @@ -13,15 +17,21 @@ error ZeroImplementationCodeSize(); error InitializationFailed(); /// @title CloneFactory -/// @notice A fairly minimal implementation of `ICloneableFactoryV3` that uses +/// @notice A fairly minimal implementation of `ICloneableFactoryV4` that uses /// Open Zeppelin `Clones` to create EIP1167 clones of a reference bytecode. The /// reference bytecode MUST implement `ICloneableV2`. /// -/// `cloneDeterministic` deploys via `CREATE2` at a pre-computable address -/// (`predictDeterministicAddress`), namespacing the caller-supplied salt by -/// `msg.sender` so a caller's `(implementation, salt)` address cannot be squatted -/// by another account. -contract CloneFactory is ICloneableFactoryV3 { +/// Two deterministic entry points, both `CREATE2`, differing only in the salt: +/// +/// - `cloneDeterministic` / `predictDeterministicAddress` namespace the +/// caller-supplied salt by `msg.sender` (see `_effectiveSalt`) so a caller's +/// `(implementation, salt)` address cannot be squatted by another account. +/// - `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt` use +/// the caller-supplied salt verbatim, so the address carries no identity and +/// anyone can deploy it. This is only safe for implementations whose +/// `initialize` takes no caller-controlled authority — read the warning on +/// `ICloneableFactoryV4.cloneDeterministicOpenSalt` before using it. +contract CloneFactory is ICloneableFactoryV4 { /// @inheritdoc ICloneableFactoryV3 function cloneDeterministic(address implementation, bytes calldata data, bytes32 salt) external returns (address) { _requireImplementationCode(implementation); @@ -39,6 +49,24 @@ contract CloneFactory is ICloneableFactoryV3 { return Clones.predictDeterministicAddress(implementation, _effectiveSalt(deployer, salt), address(this)); } + /// @inheritdoc ICloneableFactoryV4 + function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + returns (address) + { + _requireImplementationCode(implementation); + // CREATE2 clone at the caller-supplied salt verbatim: no `_effectiveSalt` + // namespacing, so the address is the same for every caller and there is + // no identity in the derivation. + address child = Clones.cloneDeterministic(implementation, salt); + return _initializeClone(implementation, child, data, salt); + } + + /// @inheritdoc ICloneableFactoryV4 + function predictDeterministicAddressOpenSalt(address implementation, bytes32 salt) external view returns (address) { + return Clones.predictDeterministicAddress(implementation, salt, address(this)); + } + /// @dev The CREATE2 salt actually used: the caller-supplied `salt` namespaced /// by the deploying account. Prevents a caller's `(implementation, salt)` /// address being front-run/squatted by another account, while still letting a @@ -69,7 +97,7 @@ contract CloneFactory is ICloneableFactoryV3 { { emit NewClone(msg.sender, implementation, child, salt, data); // Checking the return value of initialize is mandatory as per - // ICloneableFactoryV3. + // ICloneableFactoryV3 and ICloneableFactoryV4. if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) { revert InitializationFailed(); } diff --git a/src/generated/0_1_6/CloneFactory.pointers.sol b/src/generated/0_1_6/CloneFactory.pointers.sol new file mode 100644 index 0000000..886a926 --- /dev/null +++ b/src/generated/0_1_6/CloneFactory.pointers.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +// THIS FILE IS AUTOGENERATED BY ./script/BuildPointers.sol + +// This file is committed to the repository because there is a circular +// dependency between the contract and its pointers file. The contract +// needs the pointers file to exist so that it can compile, and the pointers +// file needs the contract to exist so that it can be compiled. + +/// @dev Hash of the known bytecode. +bytes32 constant BYTECODE_HASH = bytes32(0x1a16009998834f07d5ccab032c39377f6528870eec5abd47817f9467187b4012); + +/// @dev The deterministic deploy address of the contract when deployed via +/// the Zoltu factory. +address constant DEPLOYED_ADDRESS = address(0x19272bCcFcb032eaC545E74ADFa168fDeD3e8d83); + +/// @dev The creation bytecode of the contract. +bytes constant CREATION_CODE = + hex"6080604052348015600e575f80fd5b506105e48061001c5f395ff3fe608060405234801561000f575f80fd5b506004361061004a575f3560e01c806340375cb71461004e57806340419eec1461008a57806393a7e7111461009d578063fc90f455146100b0575b5f80fd5b61006161005c36600461043d565b61012e565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b61006161009836600461043d565b61015c565b6100616100ab3660046104bf565b610183565b6100616100be3660046104f8565b6040513060388201526f5af43d82803e903d91602b57fd5bf3ff60248201526014810192909252733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c8201206078820152605560439091012073ffffffffffffffffffffffffffffffffffffffff1690565b5f610138856101a5565b5f61014386846101f8565b90506101528682878787610204565b9695505050505050565b5f610166856101a5565b5f6101438661017e33865f9182526020526040902090565b6101f8565b5f61019b846100be84865f9182526020526040902090565b90505b9392505050565b8073ffffffffffffffffffffffffffffffffffffffff163b5f036101f5576040517ff432283200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50565b5f61019e83835f61033d565b5f7f3b3e5b48cfaf4a4b3b2b36425ddec21c2c507fd502d2aea55eb7ffa36b0ca20533878785888860405161023e96959493929190610567565b60405180910390a16040517f439fab910000000000000000000000000000000000000000000000000000000081527fe0e57eda3f08f2a93bbe980d3df7f9c315eac41181f58b865a13d917fe769fc39073ffffffffffffffffffffffffffffffffffffffff87169063439fab91906102bc90889088906004016105ba565b6020604051808303815f875af11580156102d8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102fc91906105cd565b14610333576040517f19b991a800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5092949350505050565b5f81471015610385576040517fcf4791810000000000000000000000000000000000000000000000000000000081524760048201526024810183905260440160405180910390fd5b763d602d80600a3d3981f3363d3d373d3d3d363d730000008460601b60e81c175f526e5af43d82803e903d91602b57fd5bf38460781b17602052826037600984f5905073ffffffffffffffffffffffffffffffffffffffff811661019e576040517fb06ebf3d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b803573ffffffffffffffffffffffffffffffffffffffff81168114610438575f80fd5b919050565b5f805f8060608587031215610450575f80fd5b61045985610415565b9350602085013567ffffffffffffffff80821115610475575f80fd5b818701915087601f830112610488575f80fd5b813581811115610496575f80fd5b8860208285010111156104a7575f80fd5b95986020929092019750949560400135945092505050565b5f805f606084860312156104d1575f80fd5b6104da84610415565b9250602084013591506104ef60408501610415565b90509250925092565b5f8060408385031215610509575f80fd5b61051283610415565b946020939093013593505050565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff8089168352808816602084015280871660408401525084606083015260a060808301526105ae60a083018486610520565b98975050505050505050565b602081525f61019b602083018486610520565b5f602082840312156105dd575f80fd5b505191905056"; + +/// @dev The runtime bytecode of the contract. +bytes constant RUNTIME_CODE = + hex"608060405234801561000f575f80fd5b506004361061004a575f3560e01c806340375cb71461004e57806340419eec1461008a57806393a7e7111461009d578063fc90f455146100b0575b5f80fd5b61006161005c36600461043d565b61012e565b60405173ffffffffffffffffffffffffffffffffffffffff909116815260200160405180910390f35b61006161009836600461043d565b61015c565b6100616100ab3660046104bf565b610183565b6100616100be3660046104f8565b6040513060388201526f5af43d82803e903d91602b57fd5bf3ff60248201526014810192909252733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c8201206078820152605560439091012073ffffffffffffffffffffffffffffffffffffffff1690565b5f610138856101a5565b5f61014386846101f8565b90506101528682878787610204565b9695505050505050565b5f610166856101a5565b5f6101438661017e33865f9182526020526040902090565b6101f8565b5f61019b846100be84865f9182526020526040902090565b90505b9392505050565b8073ffffffffffffffffffffffffffffffffffffffff163b5f036101f5576040517ff432283200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b50565b5f61019e83835f61033d565b5f7f3b3e5b48cfaf4a4b3b2b36425ddec21c2c507fd502d2aea55eb7ffa36b0ca20533878785888860405161023e96959493929190610567565b60405180910390a16040517f439fab910000000000000000000000000000000000000000000000000000000081527fe0e57eda3f08f2a93bbe980d3df7f9c315eac41181f58b865a13d917fe769fc39073ffffffffffffffffffffffffffffffffffffffff87169063439fab91906102bc90889088906004016105ba565b6020604051808303815f875af11580156102d8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102fc91906105cd565b14610333576040517f19b991a800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5092949350505050565b5f81471015610385576040517fcf4791810000000000000000000000000000000000000000000000000000000081524760048201526024810183905260440160405180910390fd5b763d602d80600a3d3981f3363d3d373d3d3d363d730000008460601b60e81c175f526e5af43d82803e903d91602b57fd5bf38460781b17602052826037600984f5905073ffffffffffffffffffffffffffffffffffffffff811661019e576040517fb06ebf3d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b803573ffffffffffffffffffffffffffffffffffffffff81168114610438575f80fd5b919050565b5f805f8060608587031215610450575f80fd5b61045985610415565b9350602085013567ffffffffffffffff80821115610475575f80fd5b818701915087601f830112610488575f80fd5b813581811115610496575f80fd5b8860208285010111156104a7575f80fd5b95986020929092019750949560400135945092505050565b5f805f606084860312156104d1575f80fd5b6104da84610415565b9250602084013591506104ef60408501610415565b90509250925092565b5f8060408385031215610509575f80fd5b61051283610415565b946020939093013593505050565b81835281816020850137505f602082840101525f60207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f840116840101905092915050565b5f73ffffffffffffffffffffffffffffffffffffffff8089168352808816602084015280871660408401525084606083015260a060808301526105ae60a083018486610520565b98975050505050505050565b602081525f61019b602083018486610520565b5f602082840312156105dd575f80fd5b505191905056"; diff --git a/src/interface/ICloneableFactoryV4.sol b/src/interface/ICloneableFactoryV4.sol new file mode 100644 index 0000000..86a992e --- /dev/null +++ b/src/interface/ICloneableFactoryV4.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.18; + +import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; + +/// @title ICloneableFactoryV4 +/// @notice Extends `ICloneableFactoryV3` with an "open salt" deterministic +/// clone. Everything `ICloneableFactoryV3` specifies is unchanged and still +/// required — `cloneDeterministic` keeps namespacing its salt by `msg.sender`, +/// and `predictDeterministicAddress` keeps taking a `deployer`. This interface +/// only ADDS a second derivation alongside it, so a factory may offer both and +/// the caller picks per deploy. +/// +/// The two derivations trade off against each other and neither dominates: +/// +/// - `cloneDeterministic` derives the `CREATE2` salt from +/// `(msg.sender, salt)`, so nobody but the caller can reach the caller's +/// address. It buys squat-resistance and pays for it with an identity baked +/// into an address: retire the deploying account and every address derived +/// from it becomes unreachable, so a pinned address can never be +/// re-established from a different account. +/// - `cloneDeterministicOpenSalt` uses `salt` verbatim, so the address is a +/// function of `(implementation, salt)` and the factory alone. It buys +/// portability — anyone can deploy it, from any account, on any chain the +/// factory exists at the same address on, forever — and pays for it with +/// squat-resistance: the address is reachable by everybody, and whoever gets +/// there first initializes it. +/// +/// Cross-network determinism is inherited from the factory, as in +/// `ICloneableFactoryV3`: when the factory is itself at the same address on +/// every chain (a Zoltu deterministic deploy), an open-salt clone address is +/// the same address on every chain, for every deployer. +interface ICloneableFactoryV4 is ICloneableFactoryV3 { + /// Deploys an EIP-1167 proxy clone of `implementation` via `CREATE2`, using + /// the caller-supplied `salt` DIRECTLY as the `CREATE2` salt. The factory + /// MUST NOT mix `msg.sender`, `tx.origin`, or any other caller-derived value + /// into the salt, so the deployed address is + /// `CREATE2(factory, salt, EIP1167(implementation))` — the same address for + /// every caller. + /// + /// Initialization is unchanged from `ICloneableFactoryV3.cloneDeterministic` + /// and MUST stay atomic with the clone: the factory MUST call + /// `ICloneableV2.initialize`, MUST NOT call anything else on the proxy + /// first, and MUST ONLY consider the clone created if `initialize` returns + /// keccak256("ICloneableV2.initialize"). MUST emit `NewClone`. + /// + /// # ONLY FOR IMPLEMENTATIONS WHOSE `initialize` TAKES NO CALLER-CONTROLLED AUTHORITY + /// + /// Dropping the `msg.sender` namespacing means anybody can deploy at this + /// address, with THEIR OWN `data`, before the party that intended to. Since + /// clone-and-initialize is atomic and `initialize` runs exactly once, the + /// first deployer's `data` sets the clone's state permanently — including + /// whatever authority that state confers. There is no recovery: the address + /// is occupied and nobody can redeploy over it. + /// + /// So this variant is safe under exactly one condition: **there must be + /// nothing for a squatter to vary.** Concretely, for every `data` any + /// account could pass at a given `salt`, the resulting contract must be the + /// contract that was intended. If two different `data` values at the same + /// salt can produce clones that differ in who controls them, what they + /// trust, or any other property that matters, the implementation does NOT + /// qualify and MUST use `cloneDeterministic` instead. + /// + /// In practice that means `initialize` MUST NOT read any address, key, + /// role, owner, admin, or other authority out of `data` (nor out of + /// `msg.sender`, which for a clone is the factory anyway). An `initialize` + /// that takes an `owner` address argument is disqualified by that argument + /// alone: a squatter passes their own address and owns the contract that + /// everybody else has already pinned. + /// + /// This is the same property that makes permissionless deterministic + /// (Zoltu-style) deployment harmless. A Zoltu deploy has no arguments at + /// all, so a stranger front-running it produces byte-for-byte the intended + /// contract and has done nothing but pay the gas. An open-salt clone has + /// arguments, so it must reach that same position by construction rather + /// than by having none. + /// + /// The intended pairing that does reach it is an address registry — such as + /// rain.deploy's — resolving authority by NAME: + /// + /// - `initialize` takes a NAME, not an address, and resolves the admin by + /// looking that name up in the registry. A squatter cannot substitute a + /// different admin, because the clone never accepts an admin address; the + /// registry decides who the name resolves to. + /// - The `salt` COMMITS to that name (it is derived from it). A squatter + /// cannot pass a different name either, because a different name is a + /// different salt, which is a different address — not the one anyone + /// pinned. + /// + /// With both halves in place the squatter's only reachable move at the + /// pinned address is to deploy exactly the intended contract. Either half + /// alone is insufficient: an admin address in `data` is substitutable even + /// if the salt commits to something, and a name that the salt does not + /// commit to is substitutable even though it goes through the registry. + /// + /// # Events + /// + /// `NewClone` is shared with `cloneDeterministic` and is emitted + /// identically, so its `sender` field is only whoever paid for this deploy; + /// for an open-salt clone it is NOT part of the address derivation, and an + /// indexer reconstructing the address MUST use `CREATE2(factory, salt, + /// EIP1167(implementation))` for clones deployed through this function. + /// + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`. MUST NOT carry authority — see above. + /// @param salt Used verbatim as the `CREATE2` salt; distinct salts yield + /// distinct clones. + /// @return New child contract address. + function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + returns (address); + + /// The address `cloneDeterministicOpenSalt(implementation, _, salt)` deploys + /// to. Takes no `deployer` because there is none in the derivation — that is + /// the entire difference from `predictDeterministicAddress`. A pure function + /// of its inputs and this factory, so it is computable (and pinnable) before + /// deploying, by anyone, and identical on every chain this factory exists at + /// the same address on. + /// + /// A non-zero code size at the returned address means the salt is already + /// taken and `cloneDeterministicOpenSalt` will revert there. Callers who + /// care WHICH contract occupies it MUST check that as well as the code + /// size — see the authority warning on `cloneDeterministicOpenSalt`. + /// + /// @param implementation The contract to clone. + /// @param salt The caller-chosen salt. + /// @return The predicted clone address. + function predictDeterministicAddressOpenSalt(address implementation, bytes32 salt) external view returns (address); +} diff --git a/src/lib/LibCloneFactoryDeploy.sol b/src/lib/LibCloneFactoryDeploy.sol index d63d7ef..431a47b 100644 --- a/src/lib/LibCloneFactoryDeploy.sol +++ b/src/lib/LibCloneFactoryDeploy.sol @@ -7,7 +7,7 @@ pragma solidity ^0.8.25; import { DEPLOYED_ADDRESS as CLONE_FACTORY_ADDR, BYTECODE_HASH as CLONE_FACTORY_HASH -} from "../generated/0_1_5/CloneFactory.pointers.sol"; +} from "../generated/0_1_6/CloneFactory.pointers.sol"; /// @title LibCloneFactoryDeploy /// @notice The deterministic Zoltu deploy address and code hash of the current diff --git a/test/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.sol b/test/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.sol new file mode 100644 index 0000000..f8b4e81 --- /dev/null +++ b/test/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.sol @@ -0,0 +1,219 @@ +// 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 {Errors} from "@openzeppelin-contracts-5.6.1/utils/Errors.sol"; +import {LibExtrospectERC1167Proxy} from "rain-extrospection-0.1.1/src/lib/LibExtrospectERC1167Proxy.sol"; +import {ICLONEABLE_V2_SUCCESS} from "../../../src/interface/ICloneableV2.sol"; +import {CloneFactory, ZeroImplementationCodeSize, InitializationFailed} from "../../../src/concrete/CloneFactory.sol"; +import {TestCloneable} from "./TestCloneable.sol"; +import {TestCloneableFailure} from "./TestCloneableFailure.sol"; + +/// @title CloneFactoryCloneDeterministicOpenSaltTest +/// @notice A test suite for `CloneFactory`'s `cloneDeterministicOpenSalt` / +/// `predictDeterministicAddressOpenSalt` functions. The defining property is +/// that the deployer is NOT in the address derivation, which is the exact +/// opposite of what `cloneDeterministic` guarantees, so the two derivations are +/// also tested against each other here. +contract CloneFactoryCloneDeterministicOpenSaltTest is Test { + /// The `CloneFactory` instance under test. Stateless, so reused everywhere. + CloneFactory internal immutable I_CLONE_FACTORY; + + constructor() { + I_CLONE_FACTORY = new CloneFactory(); + } + + /// The `CREATE2` salt is the caller-supplied salt VERBATIM — no hashing, no + /// namespacing, nothing mixed in. Pins the derivation against OZ's own + /// prediction under the raw salt, so an off-chain caller can reproduce the + /// address from `(implementation, salt, factory)` alone. + function testCloneDeterministicOpenSaltSaltIsVerbatim(address implementation, bytes32 salt) external view { + address expected = Clones.predictDeterministicAddress(implementation, salt, address(I_CLONE_FACTORY)); + assertEq(I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, salt), expected); + } + + /// The deployed clone lands at the predicted address, is an EIP1167 proxy of + /// the implementation, and is initialized with the data. `predict` therefore + /// lets a caller pin the address before deploying. + function testCloneDeterministicOpenSaltMatchesPredict(bytes32 salt, bytes memory data) external { + TestCloneable implementation = new TestCloneable(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), salt); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), data, salt); + + assertEq(child, predicted); + (bool isProxy, address proxyImplementation) = LibExtrospectERC1167Proxy.isERC1167Proxy(child.code); + assertEq(isProxy, true); + assertEq(proxyImplementation, address(implementation)); + assertEq(TestCloneable(child).sData(), data); + } + + /// THE POINT OF THIS VARIANT. The same `(implementation, 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), 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, 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, + bytes32 salt, + address alice, + address bob + ) external { + vm.assume(alice != bob); + + vm.prank(alice); + address predictedAlice = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, salt); + + vm.prank(bob); + address predictedBob = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, salt); + + assertEq(predictedAlice, predictedBob); + } + + /// The two derivations are disjoint: for any `(implementation, salt, + /// deployer)` the open-salt address is not the sender-namespaced address. + /// So adding the open variant cannot reach, block or collide with an address + /// that `cloneDeterministic` promised to a specific caller. + function testCloneDeterministicOpenSaltDiffersFromSenderNamespaced( + address implementation, + bytes32 salt, + address deployer + ) external view { + address open = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(implementation, salt); + address namespaced = I_CLONE_FACTORY.predictDeterministicAddress(implementation, salt, deployer); + assertTrue(open != namespaced); + } + + /// REGRESSION GUARD on the guarantee that must not break. 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 — many + /// clones per impl, as with the namespaced variant. + 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. It does not + /// silently return the existing clone, so a caller can never mistake + /// somebody else's already-initialized contract for their own fresh deploy. + /// Squatting is therefore loud at the point of deploy, even though it is + /// unrecoverable after it. + function testCloneDeterministicOpenSaltSecondDeployReverts( + bytes32 salt, + bytes memory dataFirst, + bytes memory dataSecond, + address alice, + address bob + ) external { + vm.assume(alice != bob); + TestCloneable implementation = new TestCloneable(); + + vm.prank(alice); + address child = I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataFirst, salt); + + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(Errors.FailedDeployment.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), dataSecond, salt); + + // The first deploy's state is untouched by the failed second one. + assertEq(TestCloneable(child).sData(), dataFirst); + } + + /// `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, so an indexer must know which entry point was called + /// to recompute the address — the `clone` field is the authoritative address. + 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].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(); + + address predicted = I_CLONE_FACTORY.predictDeterministicAddressOpenSalt(address(implementation), salt); + + vm.expectRevert(abi.encodeWithSelector(InitializationFailed.selector)); + I_CLONE_FACTORY.cloneDeterministicOpenSalt(address(implementation), abi.encode(notSuccess), 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/LibCloneFactoryDeployTaggedConstants.t.sol b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol index a13cb31..07f190d 100644 --- a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol +++ b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol @@ -22,6 +22,12 @@ import { CREATION_CODE as CLONE_FACTORY_CREATION_CODE_0_1_5, RUNTIME_CODE as CLONE_FACTORY_RUNTIME_CODE_0_1_5 } from "../../../src/generated/0_1_5/CloneFactory.pointers.sol"; +import { + BYTECODE_HASH as CLONE_FACTORY_BYTECODE_HASH_0_1_6, + DEPLOYED_ADDRESS as CLONE_FACTORY_DEPLOYED_ADDRESS_0_1_6, + CREATION_CODE as CLONE_FACTORY_CREATION_CODE_0_1_6, + RUNTIME_CODE as CLONE_FACTORY_RUNTIME_CODE_0_1_6 +} from "../../../src/generated/0_1_6/CloneFactory.pointers.sol"; /// @title LibCloneFactoryDeployTaggedConstantsTest /// @notice Each frozen per-tag `CloneFactory` snapshot must be self-consistent @@ -80,4 +86,51 @@ contract LibCloneFactoryDeployTaggedConstantsTest is Test { assertEq(deployed.codehash, CLONE_FACTORY_BYTECODE_HASH_0_1_5); assertEq(keccak256(deployed.code), CLONE_FACTORY_BYTECODE_HASH_0_1_5); } + + /// `keccak256(RUNTIME_CODE) == BYTECODE_HASH` for the tag — the pin is + /// internally consistent. + function testCloneFactory_0_1_6_RuntimeHashesToBytecodeHash() external pure { + assertEq(keccak256(CLONE_FACTORY_RUNTIME_CODE_0_1_6), CLONE_FACTORY_BYTECODE_HASH_0_1_6); + } + + /// Deploying the frozen `CREATION_CODE` via the Zoltu factory lands at the + /// recorded `DEPLOYED_ADDRESS` with the recorded codehash — the snapshot + /// reproduces its own deployment. + function testCloneFactory_0_1_6_CreationDeploysToPinnedAddress() external { + LibRainDeploy.etchZoltuFactory(vm); + address deployed = LibRainDeploy.deployZoltu(CLONE_FACTORY_CREATION_CODE_0_1_6); + assertEq(deployed, CLONE_FACTORY_DEPLOYED_ADDRESS_0_1_6); + assertEq(deployed.codehash, CLONE_FACTORY_BYTECODE_HASH_0_1_6); + assertEq(keccak256(deployed.code), CLONE_FACTORY_BYTECODE_HASH_0_1_6); + } + + /// The open-salt release's runtime code MUST expose both deterministic + /// entry points. A snapshot that pins bytecode without + /// `cloneDeterministicOpenSalt` in it would be a silently wrong pin — the + /// address would be real and the function missing. + function testCloneFactory_0_1_6_RuntimeExposesBothEntryPoints() external pure { + bytes4 cloneDeterministicSelector = bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)")); + bytes4 cloneOpenSaltSelector = bytes4(keccak256("cloneDeterministicOpenSalt(address,bytes,bytes32)")); + bytes4 predictSelector = bytes4(keccak256("predictDeterministicAddress(address,bytes32,address)")); + bytes4 predictOpenSaltSelector = bytes4(keccak256("predictDeterministicAddressOpenSalt(address,bytes32)")); + + assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, cloneDeterministicSelector)); + assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, cloneOpenSaltSelector)); + assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, predictSelector)); + assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, predictOpenSaltSelector)); + } + + /// True if the 4 byte `selector` appears anywhere in `code`. Enough to see a + /// selector in a dispatch table without decoding the dispatcher. + function _containsSelector(bytes memory code, bytes4 selector) internal pure returns (bool) { + for (uint256 i = 0; i + 4 <= code.length; i++) { + if ( + code[i] == selector[0] && code[i + 1] == selector[1] && code[i + 2] == selector[2] + && code[i + 3] == selector[3] + ) { + return true; + } + } + return false; + } } From f4635ce8a7dafc6b602c31dc7eaece92f2e89665 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 8 Aug 2026 14:30:02 +0000 Subject: [PATCH 2/7] docs+test: cross-chain portability is conditional; prove entry points dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CodeRabbit findings, both correct. Cross-network determinism needs BOTH the factory and the implementation at the same address on each chain: CREATE2 hashes the factory, and the EIP1167 creation code it hashes contains the implementation. Dropping msg.sender from the salt removes the deployer as a third thing that has to match; it does not make the other two match. Stated in ICloneableFactoryV4 and README rather than the unconditional "portable across chains" claim. The 0.1.6 snapshot's entry-point check was a byte scan, which a selector sitting in constant data passes without being dispatchable. Replaced with deploying the frozen CREATION_CODE and calling all four entry points on it. Verified discriminating: pinning 0.1.5's creation code instead reverts. Pins are unchanged — the source edits are NatSpec only. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 11 ++-- src/interface/ICloneableFactoryV4.sol | 26 +++++---- ...LibCloneFactoryDeployTaggedConstants.t.sol | 53 ++++++++++--------- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index cd21f91..3027dd4 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,13 @@ salt is derived: nobody else can reach the caller's address. - `cloneDeterministicOpenSalt` uses the caller-supplied salt verbatim, so the address is a function of `(implementation, salt)` and the factory alone — - portable across accounts and chains, but reachable by anyone. It is ONLY safe - for implementations whose `initialize` takes no caller-controlled authority; - read the NatSpec on `ICloneableFactoryV4.cloneDeterministicOpenSalt` before - using it. + every account reaches the same address, but so can anyone. That also makes it + the same address across chains, but only where both the factory and the + implementation are themselves at the same address on each chain: `CREATE2` + hashes the factory, and the EIP1167 creation code it hashes contains the + implementation. It is ONLY safe for implementations whose `initialize` takes + no caller-controlled authority; read the NatSpec on + `ICloneableFactoryV4.cloneDeterministicOpenSalt` before using it. ## Interfaces diff --git a/src/interface/ICloneableFactoryV4.sol b/src/interface/ICloneableFactoryV4.sol index 86a992e..47a15d7 100644 --- a/src/interface/ICloneableFactoryV4.sol +++ b/src/interface/ICloneableFactoryV4.sol @@ -22,15 +22,20 @@ import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; /// re-established from a different account. /// - `cloneDeterministicOpenSalt` uses `salt` verbatim, so the address is a /// function of `(implementation, salt)` and the factory alone. It buys -/// portability — anyone can deploy it, from any account, on any chain the -/// factory exists at the same address on, forever — and pays for it with -/// squat-resistance: the address is reachable by everybody, and whoever gets -/// there first initializes it. +/// portability — every account reaches the same address, forever — and pays +/// for it with squat-resistance: the address is reachable by everybody, and +/// whoever gets there first initializes it. /// -/// Cross-network determinism is inherited from the factory, as in -/// `ICloneableFactoryV3`: when the factory is itself at the same address on -/// every chain (a Zoltu deterministic deploy), an open-salt clone address is -/// the same address on every chain, for every deployer. +/// Cross-network determinism is NOT a property of either derivation on its own. +/// `CREATE2` hashes the deploying factory's address, and the EIP-1167 creation +/// code it hashes contains the implementation's address, so an open-salt clone +/// is at the same address on two chains only when BOTH the factory and the +/// implementation are at the same address on both — each deployed +/// deterministically (Zoltu-style), all the way down. Dropping `msg.sender` from +/// the salt removes the deployer as a third thing that has to match; it does not +/// make the other two match. If the implementation is deployed by an ordinary +/// nonce-dependent `CREATE` on each chain, its address differs per chain and so +/// does every clone of it, on both derivations. interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// Deploys an EIP-1167 proxy clone of `implementation` via `CREATE2`, using /// the caller-supplied `salt` DIRECTLY as the `CREATE2` salt. The factory @@ -115,8 +120,9 @@ interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// to. Takes no `deployer` because there is none in the derivation — that is /// the entire difference from `predictDeterministicAddress`. A pure function /// of its inputs and this factory, so it is computable (and pinnable) before - /// deploying, by anyone, and identical on every chain this factory exists at - /// the same address on. + /// deploying, by anyone. Identical across chains only where both this + /// factory and `implementation` are at the same address on each — see the + /// cross-network note on this interface. /// /// A non-zero code size at the returned address means the salt is already /// taken and `cloneDeterministicOpenSalt` will revert there. Callers who diff --git a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol index 07f190d..24bf13d 100644 --- a/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol +++ b/test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol @@ -4,6 +4,8 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; import {LibRainDeploy} from "rain-deploy-0.1.3/src/lib/LibRainDeploy.sol"; +import {CloneFactory} from "../../../src/concrete/CloneFactory.sol"; +import {TestCloneable} from "../concrete/TestCloneable.sol"; import { BYTECODE_HASH as CLONE_FACTORY_BYTECODE_HASH_0_1_3, DEPLOYED_ADDRESS as CLONE_FACTORY_DEPLOYED_ADDRESS_0_1_3, @@ -104,33 +106,32 @@ contract LibCloneFactoryDeployTaggedConstantsTest is Test { assertEq(keccak256(deployed.code), CLONE_FACTORY_BYTECODE_HASH_0_1_6); } - /// The open-salt release's runtime code MUST expose both deterministic - /// entry points. A snapshot that pins bytecode without - /// `cloneDeterministicOpenSalt` in it would be a silently wrong pin — the - /// address would be real and the function missing. - function testCloneFactory_0_1_6_RuntimeExposesBothEntryPoints() external pure { - bytes4 cloneDeterministicSelector = bytes4(keccak256("cloneDeterministic(address,bytes,bytes32)")); - bytes4 cloneOpenSaltSelector = bytes4(keccak256("cloneDeterministicOpenSalt(address,bytes,bytes32)")); - bytes4 predictSelector = bytes4(keccak256("predictDeterministicAddress(address,bytes32,address)")); - bytes4 predictOpenSaltSelector = bytes4(keccak256("predictDeterministicAddressOpenSalt(address,bytes32)")); + /// The open-salt release's frozen bytecode must actually SERVE all four + /// deterministic entry points, so the pin cannot record an address for + /// bytecode that is missing one. Proved by deploying the frozen + /// `CREATION_CODE` and calling every entry point on the result through the + /// `ICloneableFactoryV4` ABI: an entry point the dispatcher does not expose + /// falls through to the (absent) fallback and reverts here. A byte scan of + /// the runtime code would NOT prove this — a selector can sit in constant + /// data without being dispatchable. + function testCloneFactory_0_1_6_DeployedBytecodeServesBothEntryPoints() external { + LibRainDeploy.etchZoltuFactory(vm); + CloneFactory factory = CloneFactory(LibRainDeploy.deployZoltu(CLONE_FACTORY_CREATION_CODE_0_1_6)); + TestCloneable implementation = new TestCloneable(); - assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, cloneDeterministicSelector)); - assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, cloneOpenSaltSelector)); - assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, predictSelector)); - assertTrue(_containsSelector(CLONE_FACTORY_RUNTIME_CODE_0_1_6, predictOpenSaltSelector)); - } + bytes32 salt = keccak256("rain.factory.tagged.constants.entry.points"); + bytes memory data = hex"f100dedb0a75"; + + address predictedNamespaced = factory.predictDeterministicAddress(address(implementation), salt, address(this)); + address predictedOpen = factory.predictDeterministicAddressOpenSalt(address(implementation), salt); + assertTrue(predictedNamespaced != predictedOpen); + + address childNamespaced = factory.cloneDeterministic(address(implementation), data, salt); + address childOpen = factory.cloneDeterministicOpenSalt(address(implementation), data, salt); - /// True if the 4 byte `selector` appears anywhere in `code`. Enough to see a - /// selector in a dispatch table without decoding the dispatcher. - function _containsSelector(bytes memory code, bytes4 selector) internal pure returns (bool) { - for (uint256 i = 0; i + 4 <= code.length; i++) { - if ( - code[i] == selector[0] && code[i + 1] == selector[1] && code[i + 2] == selector[2] - && code[i + 3] == selector[3] - ) { - return true; - } - } - return false; + assertEq(childNamespaced, predictedNamespaced); + assertEq(childOpen, predictedOpen); + assertEq(TestCloneable(childNamespaced).sData(), data); + assertEq(TestCloneable(childOpen).sData(), data); } } From b6e34ad9d076271eb8e2f35e6aaac3c84e1e9cfe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 21:27:22 +0000 Subject: [PATCH 3/7] feat: open-salt derivation hashes data in and sender out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The open-salt CREATE2 salt was the caller-supplied salt verbatim, with `data` outside the derivation. CREATE2 deploys once, so the first caller's `data` was baked in permanently at an address that did not encode it: deployer irrelevance depended on consumers choosing to pass empty `data` and on auditing each implementation's `initialize` for whether it takes authority from `data`. The salt is now keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))) so the address commits to `data` and not to the deployer. A front-runner passing different `data` lands at a different address; one passing the same `data` produces the intended contract and has paid the gas. That is a property of the signature rather than a consumer convention, and it makes registry-resolved authority the ordinary empty-`data` case instead of a special pairing consumers assemble by hand. `predictDeterministicAddressOpenSalt` gains `data`, since V3's rule is that predict takes exactly the inputs of the derivation. The domain tag is load-bearing rather than decorative. The inherited `cloneDeterministic` takes arbitrary `data` at effective salt keccak256(abi.encode(deployer, salt)) — 64 bytes. An untagged keccak256(abi.encode(salt, keccak256(data))) is also 64 bytes, so an attacker holding address A could squat any open salt equal to bytes32(uint256(uint160(A))) with arbitrary data and no preimage search. The tag separates the images by both length and first word, and the interface states the disjointness as a MUST NOT on the factory. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 30 ++-- README.md | 41 +++-- src/interface/ICloneableFactoryV4.sol | 241 ++++++++++++++++---------- 3 files changed, 192 insertions(+), 120 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fcebfa8..5ed5ae8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,9 +7,9 @@ code in this repository. 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. +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. The concrete `CloneFactory` that implements these interfaces, its deployed address + codehash pins (`LibCloneFactoryDeploy`), the frozen @@ -74,12 +74,18 @@ forge build `ICloneableFactoryV3` (nothing was dropped this time, so it inherits rather than restates) and adds the open-salt variant: `cloneDeterministicOpenSalt(address, bytes, bytes32)` + - `predictDeterministicAddressOpenSalt(address, bytes32)`, which use the - caller-supplied salt verbatim so the deployer is not in the address - derivation. Only safe for implementations whose `initialize` takes no - caller-controlled authority — the NatSpec on the function is the spec for - that, and it is the deliverable of this interface as much as the two - signatures are. + `predictDeterministicAddressOpenSalt(address, bytes, bytes32)`. Their + `CREATE2` salt is + `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))` + — the deployer is out of the derivation and the initialization data is in it, + so the address commits to what was deployed rather than to who deployed it, + and a front-runner can only either land elsewhere or produce the intended + contract. `ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN` is a file-level constant, + same pattern as `ICLONEABLE_V2_SUCCESS`; it keeps the open-salt image disjoint + from the inherited `cloneDeterministic` one, which does take arbitrary `data`. + The NatSpec on the function is the spec for what the address does NOT fix + (implementations MUST NOT read `tx.origin`) and for the registry pairing, and + it is the deliverable of this interface as much as the two signatures are. - `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. @@ -101,9 +107,9 @@ relative import). - Dependencies are managed with Soldeer (`[dependencies]` in `foundry.toml` + `soldeer.lock`, vendored under `dependencies/`). The interfaces import nothing from outside this repo, so the only entry is forge-std. - `@openzeppelin-contracts`, `rain-extrospection`, - `rain-deploy` and `rain-sol-codegen` went with the deploy half and must not - come back: adding one here means concrete code has landed in a library repo. + `@openzeppelin-contracts`, `rain-extrospection`, `rain-deploy` and + `rain-sol-codegen` went with the deploy half and must not come back: adding + one here means concrete code has landed in a library repo. ## Deployment diff --git a/README.md b/README.md index 878aa1b..85492e6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ possible on the implementation side. The ideal would be that "any" contract can call an interpreter and magically be supported but there's a lot that can go wrong, for example: -- Contracts can self destruct or even [redeployed with new bytecode](https://0age.medium.com/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e) +- Contracts can self destruct or even + [redeployed with new bytecode](https://0age.medium.com/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e) - Proxies can point to new implementations and "upgrade" - Discoverability of ABIs and other metadata subject to indexer limitations @@ -41,25 +42,29 @@ Falling short of the ideal, we want to support: - Support existing patterns such as EIP1167 for clones, etc. - Avoid introducing Rain-isms as much as possible -The onchain tooling for analysis is found at https://github.com/rainprotocol/rain.extrospection +The onchain tooling for analysis is found at +https://github.com/rainprotocol/rain.extrospection The current interfaces in this repository are for - `ICloneableFactoryV4`, the current factory interface. Extends `ICloneableFactoryV3` — nothing was dropped this time, so it inherits rather than restates — and adds a second deterministic derivation, - `cloneDeterministicOpenSalt` + `predictDeterministicAddressOpenSalt`, which - use the caller-supplied salt verbatim. The two derivations trade off against - each other and neither dominates: the V3 pair namespaces the salt by - `msg.sender`, so nobody else can reach the caller's address but the deploying - account is baked into it forever; the open-salt pair puts no identity in the - derivation, so every account reaches the same address (and so can anyone). - Open-salt is therefore ONLY safe for implementations whose `initialize` takes - no caller-controlled authority, because clone-and-initialize is atomic and - first mover wins permanently. That condition, what qualifies an - implementation under it, and the registry pairing it is intended for, are - spelled out in the NatSpec on `ICloneableFactoryV4.cloneDeterministicOpenSalt` - — read it before using the function + `cloneDeterministicOpenSalt` + `predictDeterministicAddressOpenSalt`, whose + `CREATE2` salt hashes the caller-supplied salt together with the + initialization data and nothing about the caller. The two derivations differ + in what the clone's address commits to, and neither dominates: the V3 pair + namespaces the salt by `msg.sender`, so the address commits to WHO deployed + and not to WHAT — nobody else can reach the caller's address, but the + deploying account is baked into it forever and the deployer alone decides the + initial state. The open-salt pair commits to WHAT and not to WHO — every + account reaches the same address, and so can anyone, but everyone who reaches + it deploys the same contract initialized with the same bytes, because varying + either input lands somewhere else. Its cost is that the address is not + knowable until the data is final. The residual the address cannot fix — + implementations MUST NOT read `tx.origin` — and the address-registry pairing + it is intended for are spelled out in the NatSpec on + `ICloneableFactoryV4.cloneDeterministicOpenSalt` - `ICloneableFactoryV3`, deterministic-only (`cloneDeterministic` + `predictDeterministicAddress`, CREATE2 with the salt namespaced by `msg.sender`). Superseded by `ICloneableFactoryV4`, still published for @@ -77,9 +82,9 @@ The current interfaces in this repository are for #### `ICloneableV1` -This version of `ICloneable` did not have any explicit return value on success of -initialize. It is possible for contracts that do not implement `ICloneableV1` to -silently fail to initialize when cloned by an `ICloneableFactoryV1`. +This version of `ICloneable` did not have any explicit return value on success +of initialize. It is possible for contracts that do not implement `ICloneableV1` +to silently fail to initialize when cloned by an `ICloneableFactoryV1`. Newer versions of the interface include an explicit success value and check. @@ -96,4 +101,4 @@ This was suboptimal for several reasons: - Redundant work to maintain a growing list of factories The legacy interface is available as `IFactory` but it is NOT RECOMMENDED for -new contracts. \ No newline at end of file +new contracts. diff --git a/src/interface/ICloneableFactoryV4.sol b/src/interface/ICloneableFactoryV4.sol index 47a15d7..867f3fb 100644 --- a/src/interface/ICloneableFactoryV4.sol +++ b/src/interface/ICloneableFactoryV4.sol @@ -4,6 +4,12 @@ pragma solidity ^0.8.18; import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; +/// @dev Domain separator mixed into every `cloneDeterministicOpenSalt` effective +/// salt. Its only job is to keep the open-salt derivation's image disjoint from +/// every other derivation the same factory offers, so no other entry point on +/// the factory can be aimed at an open-salt address. See `ICloneableFactoryV4`. +bytes32 constant ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN = keccak256("ICloneableFactoryV4.cloneDeterministicOpenSalt"); + /// @title ICloneableFactoryV4 /// @notice Extends `ICloneableFactoryV3` with an "open salt" deterministic /// clone. Everything `ICloneableFactoryV3` specifies is unchanged and still @@ -12,19 +18,41 @@ import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; /// only ADDS a second derivation alongside it, so a factory may offer both and /// the caller picks per deploy. /// -/// The two derivations trade off against each other and neither dominates: +/// The difference between the two is which of the deployer and the +/// initialization data the clone's address commits to: +/// +/// - `cloneDeterministic` derives the `CREATE2` salt from `(msg.sender, salt)`. +/// The address commits to WHO deployed and not to WHAT was deployed. It buys +/// squat-resistance — nobody but that account can reach that address — and +/// pays with an identity baked into an address: retire the deploying account +/// and every address derived from it becomes unreachable, so a pinned address +/// can never be re-established from a different account. It also leaves +/// `data` outside the derivation, so the deployer alone decides the clone's +/// initial state at an address that says nothing about it. +/// - `cloneDeterministicOpenSalt` derives the `CREATE2` salt from +/// `(salt, data)`. The address commits to WHAT was deployed and not to WHO +/// deployed it. Every account reaches the same address — and so can anyone — +/// but every account that reaches it deploys the same contract, initialized +/// with the same bytes, because varying either input lands somewhere else. +/// +/// Neither dominates. Open-salt costs the ability to choose an address before +/// the initialization data is final: the address is not knowable until `data` +/// is, and re-deploying "the same" clone with corrected `data` is a different +/// address. Sender-namespacing costs portability across accounts. A consumer +/// pinning an open-salt address must be able to reproduce the exact `data` +/// bytes, ABI encoding and all, since a byte of difference is a different +/// address. +/// +/// The open-salt effective `CREATE2` salt is fixed by this interface as +/// +/// ``` +/// keccak256(abi.encode( +/// ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data) +/// )) +/// ``` /// -/// - `cloneDeterministic` derives the `CREATE2` salt from -/// `(msg.sender, salt)`, so nobody but the caller can reach the caller's -/// address. It buys squat-resistance and pays for it with an identity baked -/// into an address: retire the deploying account and every address derived -/// from it becomes unreachable, so a pinned address can never be -/// re-established from a different account. -/// - `cloneDeterministicOpenSalt` uses `salt` verbatim, so the address is a -/// function of `(implementation, salt)` and the factory alone. It buys -/// portability — every account reaches the same address, forever — and pays -/// for it with squat-resistance: the address is reachable by everybody, and -/// whoever gets there first initializes it. +/// so third parties can recompute it, and `predictDeterministicAddressOpenSalt` +/// is the factory saying the same thing. /// /// Cross-network determinism is NOT a property of either derivation on its own. /// `CREATE2` hashes the deploying factory's address, and the EIP-1167 creation @@ -32,105 +60,138 @@ import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; /// is at the same address on two chains only when BOTH the factory and the /// implementation are at the same address on both — each deployed /// deterministically (Zoltu-style), all the way down. Dropping `msg.sender` from -/// the salt removes the deployer as a third thing that has to match; it does not -/// make the other two match. If the implementation is deployed by an ordinary -/// nonce-dependent `CREATE` on each chain, its address differs per chain and so -/// does every clone of it, on both derivations. +/// the derivation removes the deployer as a third thing that has to match; it +/// does not make the other two match. If the implementation is deployed by an +/// ordinary nonce-dependent `CREATE` on each chain, its address differs per +/// chain and so does every clone of it, on both derivations. interface ICloneableFactoryV4 is ICloneableFactoryV3 { - /// Deploys an EIP-1167 proxy clone of `implementation` via `CREATE2`, using - /// the caller-supplied `salt` DIRECTLY as the `CREATE2` salt. The factory - /// MUST NOT mix `msg.sender`, `tx.origin`, or any other caller-derived value - /// into the salt, so the deployed address is - /// `CREATE2(factory, salt, EIP1167(implementation))` — the same address for - /// every caller. + /// Deploys an EIP-1167 proxy clone of `implementation` via `CREATE2` at an + /// address that does not depend on the caller and does depend on the + /// initialization data. + /// + /// The factory MUST use + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))` + /// as the `CREATE2` salt, so the deployed address is a pure function of + /// `(factory, implementation, salt, data)`. The factory MUST NOT mix + /// `msg.sender`, `tx.origin`, or any other caller-derived value into it. /// /// Initialization is unchanged from `ICloneableFactoryV3.cloneDeterministic` /// and MUST stay atomic with the clone: the factory MUST call - /// `ICloneableV2.initialize`, MUST NOT call anything else on the proxy - /// first, and MUST ONLY consider the clone created if `initialize` returns - /// keccak256("ICloneableV2.initialize"). MUST emit `NewClone`. - /// - /// # ONLY FOR IMPLEMENTATIONS WHOSE `initialize` TAKES NO CALLER-CONTROLLED AUTHORITY - /// - /// Dropping the `msg.sender` namespacing means anybody can deploy at this - /// address, with THEIR OWN `data`, before the party that intended to. Since - /// clone-and-initialize is atomic and `initialize` runs exactly once, the - /// first deployer's `data` sets the clone's state permanently — including - /// whatever authority that state confers. There is no recovery: the address - /// is occupied and nobody can redeploy over it. - /// - /// So this variant is safe under exactly one condition: **there must be - /// nothing for a squatter to vary.** Concretely, for every `data` any - /// account could pass at a given `salt`, the resulting contract must be the - /// contract that was intended. If two different `data` values at the same - /// salt can produce clones that differ in who controls them, what they - /// trust, or any other property that matters, the implementation does NOT - /// qualify and MUST use `cloneDeterministic` instead. - /// - /// In practice that means `initialize` MUST NOT read any address, key, - /// role, owner, admin, or other authority out of `data` (nor out of - /// `msg.sender`, which for a clone is the factory anyway). An `initialize` - /// that takes an `owner` address argument is disqualified by that argument - /// alone: a squatter passes their own address and owns the contract that - /// everybody else has already pinned. - /// - /// This is the same property that makes permissionless deterministic - /// (Zoltu-style) deployment harmless. A Zoltu deploy has no arguments at - /// all, so a stranger front-running it produces byte-for-byte the intended - /// contract and has done nothing but pay the gas. An open-salt clone has - /// arguments, so it must reach that same position by construction rather - /// than by having none. - /// - /// The intended pairing that does reach it is an address registry — such as - /// rain.deploy's — resolving authority by NAME: - /// - /// - `initialize` takes a NAME, not an address, and resolves the admin by - /// looking that name up in the registry. A squatter cannot substitute a - /// different admin, because the clone never accepts an admin address; the - /// registry decides who the name resolves to. - /// - The `salt` COMMITS to that name (it is derived from it). A squatter - /// cannot pass a different name either, because a different name is a - /// different salt, which is a different address — not the one anyone - /// pinned. - /// - /// With both halves in place the squatter's only reachable move at the - /// pinned address is to deploy exactly the intended contract. Either half - /// alone is insufficient: an admin address in `data` is substitutable even - /// if the salt commits to something, and a name that the salt does not - /// commit to is substitutable even though it goes through the registry. + /// `ICloneableV2.initialize` with `data` verbatim, MUST NOT call anything + /// else on the proxy first, and MUST ONLY consider the clone created if + /// `initialize` returns keccak256("ICloneableV2.initialize"). MUST emit + /// `NewClone`. + /// + /// # Why hashing `data` into the salt is the whole point + /// + /// Without the `msg.sender` namespacing, anybody can deploy at this address + /// before the party that intended to, and since clone-and-initialize is + /// atomic and `initialize` runs exactly once, whoever gets there first sets + /// the clone's state permanently. There is no recovery: the address is + /// occupied and nobody can redeploy over it. + /// + /// That is only dangerous if the first deployer has anything to vary. + /// Because `data` is inside the derivation, they do not: + /// + /// - A front-runner passing DIFFERENT `data` derives a DIFFERENT address. + /// The address anyone pinned is untouched; the front-runner has deployed + /// their own contract at their own address, at their own expense. + /// - A front-runner passing the SAME `data` produces the contract that was + /// intended, initialized with the bytes that were intended, and has done + /// nothing but pay the gas. + /// + /// This is the same position that makes permissionless deterministic + /// (Zoltu-style) deployment harmless — a Zoltu deploy has no arguments, so + /// front-running it produces byte-for-byte the intended contract — reached + /// WITH arguments, by putting the arguments in the address rather than by + /// having none. It is a property of this signature, not a condition on the + /// implementation being cloned, so there is no per-implementation audit of + /// "could a squatter pass something worse" to get wrong. + /// + /// # What the address does NOT fix, which implementations MUST respect + /// + /// The address fixes `data`. It cannot fix anything `initialize` reads that + /// is not `data`, so the deployer keeps exactly one lever: WHEN the deploy + /// lands, and therefore which chain state `initialize` observes. + /// + /// - The implementation MUST NOT read `tx.origin`, directly or through + /// anything it calls during initialization. `tx.origin` is the deployer, + /// and it is the one remaining channel by which the deployer could reach + /// initial state. (`msg.sender` during `initialize` is the factory, which + /// is the same for every caller and therefore harmless.) + /// - Anything else `initialize` resolves from chain state resolves the same + /// way for every caller at a given block. An address registry — such as + /// rain.deploy's — is the intended shape here: `initialize` resolves the + /// admin by NAME from the registry rather than taking an admin address, + /// and the name, being part of `data`, is committed to by the address. + /// A front-runner resolves the same admin the intended deployer would + /// have. While the name is unbound the registry read reverts, so the + /// clone cannot be deployed at all, and the front-running window only + /// opens once the binding exists. A clone that resolves once during + /// `initialize` and stores the answer is unaffected by any later + /// rebinding. + /// - Registry-resolved authority is now the ordinary case rather than a + /// special one: it is simply `data` that names things instead of naming + /// addresses. `data` MAY be empty, and an implementation that resolves + /// everything from the registry will pass empty `data`. + /// + /// # Obligation on the factory, not on the consumer + /// + /// The guarantee above holds only while no OTHER entry point on the same + /// factory can `CREATE2` at an effective salt in this derivation's image + /// with caller-supplied initialization data. A factory implementing this + /// interface MUST NOT expose one. That is what + /// `ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN` is for: it separates this + /// derivation from the inherited `cloneDeterministic` one — which does take + /// arbitrary `data` — by both a domain tag and a preimage length, so + /// aiming `cloneDeterministic` at an open-salt address requires a keccak256 + /// preimage rather than a choice of salt. /// /// # Events /// /// `NewClone` is shared with `cloneDeterministic` and is emitted - /// identically, so its `sender` field is only whoever paid for this deploy; - /// for an open-salt clone it is NOT part of the address derivation, and an - /// indexer reconstructing the address MUST use `CREATE2(factory, salt, - /// EIP1167(implementation))` for clones deployed through this function. + /// identically, with the caller-supplied `salt` — NOT the effective salt. + /// Its `sender` field is only whoever paid for this deploy and is not part + /// of the address derivation, but `salt` and `data` together are the whole + /// of it, so the event still carries the full deterministic deploy that + /// `ICloneableFactoryV3.NewClone` promises. An indexer that wants to verify + /// the address rather than take the emitted one MUST pick the derivation: + /// the two cannot both produce the emitted `clone`, so trying both and + /// keeping the match is well defined. /// /// @param implementation The contract to clone. - /// @param data As per `ICloneableV2`. MUST NOT carry authority — see above. - /// @param salt Used verbatim as the `CREATE2` salt; distinct salts yield - /// distinct clones. + /// @param data As per `ICloneableV2`, and part of the address derivation. + /// MAY be empty. + /// @param salt Caller-chosen salt. Distinguishes clones that share an + /// implementation and `data`; distinct `(salt, data)` pairs yield distinct + /// clones. /// @return New child contract address. function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt) external returns (address); - /// The address `cloneDeterministicOpenSalt(implementation, _, salt)` deploys - /// to. Takes no `deployer` because there is none in the derivation — that is - /// the entire difference from `predictDeterministicAddress`. A pure function - /// of its inputs and this factory, so it is computable (and pinnable) before + /// The address `cloneDeterministicOpenSalt(implementation, data, salt)` + /// deploys to. Takes `data` because `data` is in the derivation, and takes + /// no `deployer` because the deployer is not — that is the entire + /// difference from `predictDeterministicAddress`. A pure function of its + /// inputs and this factory, so it is computable (and pinnable) before /// deploying, by anyone. Identical across chains only where both this /// factory and `implementation` are at the same address on each — see the /// cross-network note on this interface. /// - /// A non-zero code size at the returned address means the salt is already - /// taken and `cloneDeterministicOpenSalt` will revert there. Callers who - /// care WHICH contract occupies it MUST check that as well as the code - /// size — see the authority warning on `cloneDeterministicOpenSalt`. + /// A non-zero code size at the returned address means this exact + /// `(implementation, data, salt)` has already been deployed by somebody and + /// `cloneDeterministicOpenSalt` will revert there. Since nothing else can + /// be deployed there, what occupies it is the clone that was asked for, + /// initialized with the bytes that were asked for. /// /// @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, bytes32 salt) external view returns (address); + function predictDeterministicAddressOpenSalt(address implementation, bytes calldata data, bytes32 salt) + external + view + returns (address); } From bc3d6ee0820f64918f173099b6a1bd8d667bdf04 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 20 Aug 2026 13:36:32 +0000 Subject: [PATCH 4/7] feat: symmetric two-tag clone salt derivation Both deterministic entry points now lead their CREATE2 preimage with an explicit, string-derived domain tag as the first hashed word, so the two images are disjoint by construction rather than by the old asymmetric length/non-address-shape argument: - namespaced (cloneDeterministic / predictDeterministicAddress): keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt)) - open-salt (cloneDeterministicOpenSalt / predictDeterministicAddressOpenSalt): keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))) Replace the opaque open-salt domain string with a purpose-named "rain.factory.clone.opensalt" and add "rain.factory.clone.namespaced". data stays in the open-salt derivation and out of the namespaced one. Rewrite the disjointness NatSpec to argue from the distinct first-word tags an attacker cannot set. Add a V3 @dev pointer so a V4 factory's namespaced derivation bytes are discoverable, and refresh the CLAUDE.md V4 entry. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 29 ++++--- src/interface/ICloneableFactoryV3.sol | 6 ++ src/interface/ICloneableFactoryV4.sol | 115 +++++++++++++++++--------- 3 files changed, 100 insertions(+), 50 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5ed5ae8..b5f8e54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,18 +74,23 @@ forge build `ICloneableFactoryV3` (nothing was dropped this time, so it inherits rather than restates) and adds the open-salt variant: `cloneDeterministicOpenSalt(address, bytes, bytes32)` + - `predictDeterministicAddressOpenSalt(address, bytes, bytes32)`. Their - `CREATE2` salt is - `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))` - — the deployer is out of the derivation and the initialization data is in it, - so the address commits to what was deployed rather than to who deployed it, - and a front-runner can only either land elsewhere or produce the intended - contract. `ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN` is a file-level constant, - same pattern as `ICLONEABLE_V2_SUCCESS`; it keeps the open-salt image disjoint - from the inherited `cloneDeterministic` one, which does take arbitrary `data`. - The NatSpec on the function is the spec for what the address does NOT fix - (implementations MUST NOT read `tx.origin`) and for the registry pairing, and - it is the deliverable of this interface as much as the two signatures are. + `predictDeterministicAddressOpenSalt(address, bytes, bytes32)`, and pins BOTH + derivations to exact bytes. Each effective `CREATE2` salt is a `keccak256` + over a 96-byte preimage led by a distinct, string-derived domain tag the + caller cannot set: the namespaced pair uses + `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))` + and the open-salt pair uses + `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. + Open-salt drops the deployer and hashes the initialization data in, so its + address commits to what was deployed rather than to who deployed it, and a + front-runner can only either land elsewhere or produce the intended contract. + Both domains are file-level constants, same pattern as + `ICLONEABLE_V2_SUCCESS`; because the two tags are distinct fixed first words, + the two images are disjoint BY CONSTRUCTION — no caller can aim one + derivation's entry point at the other's address. The NatSpec on the function + is the spec for what the address does NOT fix (implementations MUST NOT read + `tx.origin`) and for the registry pairing, and it is the deliverable of this + interface as much as the two signatures are. - `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. diff --git a/src/interface/ICloneableFactoryV3.sol b/src/interface/ICloneableFactoryV3.sol index 31214d9..7d3fcc4 100644 --- a/src/interface/ICloneableFactoryV3.sol +++ b/src/interface/ICloneableFactoryV3.sol @@ -43,6 +43,12 @@ interface ICloneableFactoryV3 { /// the string "ICloneableV2.initialize". MUST emit `NewClone` with the /// implementation and clone address. /// + /// @dev A factory that also implements `ICloneableFactoryV4` pins this + /// `msg.sender` namespacing to exact bytes: the effective `CREATE2` salt is + /// `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))`. + /// See that interface for the full derivation and its disjointness from the + /// open-salt one. + /// /// @param implementation The contract to clone. /// @param data As per `ICloneableV2`. /// @param salt Caller-chosen salt; distinct salts yield distinct clones. diff --git a/src/interface/ICloneableFactoryV4.sol b/src/interface/ICloneableFactoryV4.sol index 867f3fb..c86d82e 100644 --- a/src/interface/ICloneableFactoryV4.sol +++ b/src/interface/ICloneableFactoryV4.sol @@ -4,36 +4,73 @@ pragma solidity ^0.8.18; import {ICloneableFactoryV3} from "./ICloneableFactoryV3.sol"; -/// @dev Domain separator mixed into every `cloneDeterministicOpenSalt` effective -/// salt. Its only job is to keep the open-salt derivation's image disjoint from -/// every other derivation the same factory offers, so no other entry point on -/// the factory can be aimed at an open-salt address. See `ICloneableFactoryV4`. -bytes32 constant ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN = keccak256("ICloneableFactoryV4.cloneDeterministicOpenSalt"); +/// @dev Domain tag hashed as the FIRST word of the `cloneDeterministic` / +/// `predictDeterministicAddress` effective `CREATE2` salt on a factory that +/// implements `ICloneableFactoryV4`. String-derived so the literal is its own +/// documentation. Its job is to keep the namespaced derivation's image disjoint +/// from the open-salt one BY CONSTRUCTION: the two tags are distinct fixed +/// words and no caller can place either in word 0 of the other derivation, so +/// neither entry point can be aimed at an address the other produces. See +/// `ICloneableFactoryV4`. +bytes32 constant ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN = keccak256("rain.factory.clone.namespaced"); + +/// @dev Domain tag hashed as the FIRST word of the `cloneDeterministicOpenSalt` +/// / `predictDeterministicAddressOpenSalt` effective `CREATE2` salt. +/// String-derived so the literal is its own documentation. Pairs with +/// `ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN`: the two are distinct fixed words, +/// so the open-salt and namespaced images cannot overlap and no other entry +/// point on the factory can be aimed at an open-salt address. See +/// `ICloneableFactoryV4`. +bytes32 constant ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN = keccak256("rain.factory.clone.opensalt"); /// @title ICloneableFactoryV4 /// @notice Extends `ICloneableFactoryV3` with an "open salt" deterministic /// clone. Everything `ICloneableFactoryV3` specifies is unchanged and still /// required — `cloneDeterministic` keeps namespacing its salt by `msg.sender`, /// and `predictDeterministicAddress` keeps taking a `deployer`. This interface -/// only ADDS a second derivation alongside it, so a factory may offer both and -/// the caller picks per deploy. +/// ADDS a second derivation alongside it, so a factory may offer both and the +/// caller picks per deploy, and it PINS both derivations to exact bytes: V3 +/// mandates only the `msg.sender` namespacing as a property, and V4 fixes the +/// whole preimage of each. +/// +/// Both effective `CREATE2` salts are a `keccak256` over a 96-byte preimage +/// whose FIRST word is a distinct, string-derived domain tag the caller cannot +/// set: +/// +/// ``` +/// // cloneDeterministic / predictDeterministicAddress (the namespaced pair) +/// keccak256(abi.encode( +/// ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt +/// )) +/// +/// // cloneDeterministicOpenSalt / predictDeterministicAddressOpenSalt +/// keccak256(abi.encode( +/// ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data) +/// )) +/// ``` +/// +/// so third parties can recompute either, and the two `predict…` functions are +/// the factory saying the same thing. The two tags differ, so the two images +/// are disjoint by construction — see the disjointness note on +/// `cloneDeterministicOpenSalt`. /// /// The difference between the two is which of the deployer and the /// initialization data the clone's address commits to: /// -/// - `cloneDeterministic` derives the `CREATE2` salt from `(msg.sender, salt)`. -/// The address commits to WHO deployed and not to WHAT was deployed. It buys -/// squat-resistance — nobody but that account can reach that address — and -/// pays with an identity baked into an address: retire the deploying account -/// and every address derived from it becomes unreachable, so a pinned address -/// can never be re-established from a different account. It also leaves -/// `data` outside the derivation, so the deployer alone decides the clone's -/// initial state at an address that says nothing about it. -/// - `cloneDeterministicOpenSalt` derives the `CREATE2` salt from -/// `(salt, data)`. The address commits to WHAT was deployed and not to WHO -/// deployed it. Every account reaches the same address — and so can anyone — -/// but every account that reaches it deploys the same contract, initialized -/// with the same bytes, because varying either input lands somewhere else. +/// - `cloneDeterministic` derives its salt from `msg.sender` and `salt` (behind +/// the namespaced tag). The address commits to WHO deployed and not to WHAT +/// was deployed. It buys squat-resistance — nobody but that account can reach +/// that address — and pays with an identity baked into an address: retire the +/// deploying account and every address derived from it becomes unreachable, +/// so a pinned address can never be re-established from a different account. +/// It also leaves `data` outside the derivation, so the deployer alone +/// decides the clone's initial state at an address that says nothing about it. +/// - `cloneDeterministicOpenSalt` derives its salt from `salt` and `data` +/// (behind the open-salt tag). The address commits to WHAT was deployed and +/// not to WHO deployed it. Every account reaches the same address — and so +/// can anyone — but every account that reaches it deploys the same contract, +/// initialized with the same bytes, because varying either input lands +/// somewhere else. /// /// Neither dominates. Open-salt costs the ability to choose an address before /// the initialization data is final: the address is not knowable until `data` @@ -43,17 +80,6 @@ bytes32 constant ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN = keccak256("ICloneableF /// bytes, ABI encoding and all, since a byte of difference is a different /// address. /// -/// The open-salt effective `CREATE2` salt is fixed by this interface as -/// -/// ``` -/// keccak256(abi.encode( -/// ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data) -/// )) -/// ``` -/// -/// so third parties can recompute it, and `predictDeterministicAddressOpenSalt` -/// is the factory saying the same thing. -/// /// Cross-network determinism is NOT a property of either derivation on its own. /// `CREATE2` hashes the deploying factory's address, and the EIP-1167 creation /// code it hashes contains the implementation's address, so an open-salt clone @@ -139,13 +165,26 @@ interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// /// The guarantee above holds only while no OTHER entry point on the same /// factory can `CREATE2` at an effective salt in this derivation's image - /// with caller-supplied initialization data. A factory implementing this - /// interface MUST NOT expose one. That is what - /// `ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN` is for: it separates this - /// derivation from the inherited `cloneDeterministic` one — which does take - /// arbitrary `data` — by both a domain tag and a preimage length, so - /// aiming `cloneDeterministic` at an open-salt address requires a keccak256 - /// preimage rather than a choice of salt. + /// with caller-supplied initialization data. The inherited + /// `cloneDeterministic` is exactly such an entry point — it takes arbitrary + /// `data` — so the two derivations MUST NOT share an effective-salt image, + /// and a factory implementing this interface MUST NOT expose any entry point + /// that does. + /// + /// They do not overlap, by construction. Both preimages are 96 bytes whose + /// FIRST word is a fixed domain tag no caller can set: + /// `cloneDeterministic` hashes + /// `abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt)` + /// and this function hashes + /// `abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data))`. + /// The two domain constants are distinct `keccak256` outputs, so the two + /// preimage sets are disjoint in their first word alone. A caller on the + /// namespaced path chooses only words 1 and 2 (`msg.sender` and `salt`); a + /// caller here chooses only words 1 and 2 (`salt` and `keccak256(data)`); + /// neither can place the other derivation's tag in word 0, so neither can + /// aim its entry point at an address the other produces. The disjointness is + /// a property of the two fixed tags — not of a preimage length an attacker + /// might match or a value an attacker might fail to reach. /// /// # Events /// From 7820b076387e001ade42b7cba566b3d09a0309ee Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 20 Aug 2026 13:40:21 +0000 Subject: [PATCH 5/7] docs: trim CLAUDE.md under the 4096-byte agent-context cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The V4 architecture entry pushed CLAUDE.md over rainix's agent-context cap (rainlanguage/rainix#298). Cut the discoverable content the cap targets — the build/CI command list, the per-interface architecture catalog (the V4 derivation is specified authoritatively in the ICloneableFactoryV4 NatSpec), and the deploy-target list — keeping only the split boundary, the dependency ban, and the pragma/SPDX/release rulings whose rationale is not recoverable from the code. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 160 +++++++++--------------------------------------------- 1 file changed, 26 insertions(+), 134 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b5f8e54..ae79c33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,138 +1,30 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with -code in this repository. +## What this repo is, and the boundary that must hold -## 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. - -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 is the **library** half of the library/deploy split +(rainlanguage/rain.factory#46): `ICloneable*` interfaces only. The concrete +`CloneFactory`, its address/codehash pins, the `src/generated//` snapshots, +`script/Deploy.sol` and every test 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`. - -License: LicenseRef-DCL-1.0 (DecentraLicense). All source files must include -SPDX headers. - -## Build & Test Commands - -This project uses **Nix + Foundry (Forge)**. Enter the dev shell first: - -```bash -nix develop -``` - -Then use rainix tasks: - -```bash -# Static analysis (Slither) -nix develop -c rainix-sol-static - -# License/legal checks (REUSE compliance) -nix develop -c rainix-sol-legal - -# Prelude (dependency setup, run before other tasks) -nix develop -c rainix-sol-prelude - -# Runs, but there is no test suite here: the interfaces have no behaviour to -# test. The tests that exercise them live in rain.factory.deploy, against the -# concrete. -nix develop -c rainix-sol-test -``` - -Direct Forge commands also work inside the nix shell: - -```bash -# Build -forge build -``` - -## 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. Superseded by - `ICloneableFactoryV3`/`ICloneableFactoryV4` for the concrete factory in - rain.factory.deploy; 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`, because the non-deterministic `clone()` was - intentionally dropped. Still published for consumers pinned to it. -- `src/interface/ICloneableFactoryV4.sol` — Current factory interface. Extends - `ICloneableFactoryV3` (nothing was dropped this time, so it inherits rather - than restates) and adds the open-salt variant: - `cloneDeterministicOpenSalt(address, bytes, bytes32)` + - `predictDeterministicAddressOpenSalt(address, bytes, bytes32)`, and pins BOTH - derivations to exact bytes. Each effective `CREATE2` salt is a `keccak256` - over a 96-byte preimage led by a distinct, string-derived domain tag the - caller cannot set: the namespaced pair uses - `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN, msg.sender, salt))` - and the open-salt pair uses - `keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`. - Open-salt drops the deployer and hashes the initialization data in, so its - address commits to what was deployed rather than to who deployed it, and a - front-runner can only either land elsewhere or produce the intended contract. - Both domains are file-level constants, same pattern as - `ICLONEABLE_V2_SUCCESS`; because the two tags are distinct fixed first words, - the two images are disjoint BY CONSTRUCTION — no caller can aim one - derivation's entry point at the other's address. The NatSpec on the function - is the spec for what the address does NOT fix (implementations MUST NOT read - `tx.origin`) and for the registry pairing, and it is the deliverable of this - interface as much as the two signatures are. -- `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, - `ICloneableFactoryV1`, `IFactory`). Do not use for new work. - -`src/` holds nothing else. No interface here imports anything outside this repo -— no third-party library, no concrete contract — which is what makes this half a -standalone publish. Interface-to-interface inheritance within `src/interface/` -is allowed and `ICloneableFactoryV4` uses it (`is ICloneableFactoryV3`, via a -relative import). - -## 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. -- EVM target: Cancun -- Optimizer: enabled, 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 - from outside this repo, so the only entry is forge-std. - `@openzeppelin-contracts`, `rain-extrospection`, `rain-deploy` and - `rain-sol-codegen` went with the deploy half and must not come back: adding - one here means concrete code has landed in a library repo. - -## Deployment - -Nothing in this repo is deployed. The deterministic Zoltu deploy of the concrete -`CloneFactory`, its canonical address and codehash, and the deploy scripts -targeting Arbitrum, Base, Base Sepolia, Flare and Polygon are all in -rain.factory.deploy. - -## Releases - -Library repo, so `package-release.yaml` runs `rainix-autopublish`: -`[package].version` in `foundry.toml` is the NEXT, unpublished version, and a -content change on merge publishes it and bumps to the next. Nothing here is -tag-released, and no snapshot is frozen — that lifecycle belongs to the deploy -half. - -## CI - -GitHub Actions runs three parallel jobs on every push: `rainix-sol-test`, -`rainix-sol-static`, `rainix-sol-legal`. There are no fork tests and no RPC -secrets are needed. +publish as `rain-factory-deploy`; this repo publishes as `rain-factory`. + +- No concrete, no deploy pins, no deploy script, no tests belong here — an + interface declares no behaviour, so there is nothing here to test. Adding any + of them means the split has been undone. +- No dependency outside forge-std belongs here. `@openzeppelin-contracts`, + `rain-extrospection`, `rain-deploy` and `rain-sol-codegen` went with the + deploy half: adding one back means concrete code has landed in a library repo. +- Nothing here is deployed, tag-released or snapshot-frozen — that lifecycle is + the deploy half's. + +## Conventions that are not recoverable from the code + +- Interfaces float the `^` pragma (`^0.8.18`) so a downstream soldeer consumer + on a different `0.8.x` can still compile them. The `=0.8.25` exact-pin rule is + for concrete contracts, scripts and tests, which are not here. +- Every source file carries an SPDX header; the license is LicenseRef-DCL-1.0 + (REUSE / `rainix-sol-legal` enforces it). +- Release is `rainix-autopublish`: `[package].version` in `foundry.toml` is the + NEXT, unpublished version, and a content change on merge publishes that + version and bumps to the next. From 9dabdca61ea1aabe8b40750ff832d7b3fa9571a3 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 20 Aug 2026 13:41:17 +0000 Subject: [PATCH 6/7] docs: hyphenate self-destruct in README (CodeRabbit) Co-Authored-By: Claude Opus 4.8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85492e6..e9ef73f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ possible on the implementation side. The ideal would be that "any" contract can call an interpreter and magically be supported but there's a lot that can go wrong, for example: -- Contracts can self destruct or even +- Contracts can self-destruct or even be [redeployed with new bytecode](https://0age.medium.com/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e) - Proxies can point to new implementations and "upgrade" - Discoverability of ABIs and other metadata subject to indexer limitations From 776276c3bf2620335127d9a77c9f66a21a05a181 Mon Sep 17 00:00:00 2001 From: David Meister Date: Thu, 20 Aug 2026 14:00:32 +0000 Subject: [PATCH 7/7] docs: strip historical/version-evolution narration Comments and docs describe current behavior only, not how the design evolved. Remove version-evolution framing from the ICloneableFactoryV4 NatSpec (the unchanged/keeps/ADDS/V3-vs-V4 @notice, "unchanged from" on atomic init, "now the ordinary case rather than", and the contrast-with- the-superseded-design disjointness clause) and the version-history in CLAUDE.md's Architecture list (superseded/dropped/went-with-and-must-not- come-back). Restated as current facts and security rationale only. No behavior, constant, formula, or signature changed. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 25 +++++++++++------------ src/interface/ICloneableFactoryV4.sol | 29 ++++++++++++--------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2b34e7..7d53675 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,22 +36,21 @@ live against the concrete in rain.factory.deploy. `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. Superseded by - `ICloneableFactoryV4` for the concrete factory in rain.factory.deploy; still - published for other consumers. + 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`, because the non-deterministic `clone()` was - intentionally dropped. Superseded by `ICloneableFactoryV4`, still published - for consumers pinned to it. + NOT extend `ICloneableFactoryV2` and has no non-deterministic `clone()`. Still + published for consumers pinned to it. - `src/interface/ICloneableFactoryV4.sol` — Current factory interface. Extends - `ICloneableFactoryV3` and adds the open-salt pair `cloneDeterministicOpenSalt` - / `predictDeterministicAddressOpenSalt`. Both derivations are pinned to exact - bytes, each `keccak256`-ing a 96-byte preimage led by a distinct - string-derived domain tag, so the two images are disjoint by construction. The - full spec is the NatSpec on the interface. + `ICloneableFactoryV3` and defines the open-salt pair + `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`. Both + derivations are pinned to exact bytes, each `keccak256`-ing a 96-byte preimage + led by a distinct string-derived domain tag, so the two images are disjoint by + construction. The full spec is the NatSpec on the interface. - `src/interface/deprecated/` — Legacy interfaces (`ICloneableV1`, `ICloneableFactoryV1`, `IFactory`). Do not use for new work. @@ -70,6 +69,6 @@ live against the concrete in rain.factory.deploy. - 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` went with the - deploy half and must not come back: adding one here means concrete code has + `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/src/interface/ICloneableFactoryV4.sol b/src/interface/ICloneableFactoryV4.sol index c86d82e..b3ff5f1 100644 --- a/src/interface/ICloneableFactoryV4.sol +++ b/src/interface/ICloneableFactoryV4.sol @@ -24,14 +24,12 @@ bytes32 constant ICLONEABLE_FACTORY_V4_NAMESPACED_DOMAIN = keccak256("rain.facto bytes32 constant ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN = keccak256("rain.factory.clone.opensalt"); /// @title ICloneableFactoryV4 -/// @notice Extends `ICloneableFactoryV3` with an "open salt" deterministic -/// clone. Everything `ICloneableFactoryV3` specifies is unchanged and still -/// required — `cloneDeterministic` keeps namespacing its salt by `msg.sender`, -/// and `predictDeterministicAddress` keeps taking a `deployer`. This interface -/// ADDS a second derivation alongside it, so a factory may offer both and the -/// caller picks per deploy, and it PINS both derivations to exact bytes: V3 -/// mandates only the `msg.sender` namespacing as a property, and V4 fixes the -/// whole preimage of each. +/// @notice A factory with two deterministic-clone derivations, both pinned to +/// exact bytes. The namespaced pair — `cloneDeterministic`, which namespaces its +/// salt by `msg.sender`, and `predictDeterministicAddress`, which takes a +/// `deployer` — is inherited from `ICloneableFactoryV3`. The open-salt pair, +/// `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt`, is +/// defined here. A factory may offer both, and the caller picks per deploy. /// /// Both effective `CREATE2` salts are a `keccak256` over a 96-byte preimage /// whose FIRST word is a distinct, string-derived domain tag the caller cannot @@ -101,8 +99,8 @@ interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// `(factory, implementation, salt, data)`. The factory MUST NOT mix /// `msg.sender`, `tx.origin`, or any other caller-derived value into it. /// - /// Initialization is unchanged from `ICloneableFactoryV3.cloneDeterministic` - /// and MUST stay atomic with the clone: the factory MUST call + /// Initialization MUST be atomic with the clone, per the shared spec in + /// `ICloneableFactoryV3.cloneDeterministic`: the factory MUST call /// `ICloneableV2.initialize` with `data` verbatim, MUST NOT call anything /// else on the proxy first, and MUST ONLY consider the clone created if /// `initialize` returns keccak256("ICloneableV2.initialize"). MUST emit @@ -156,10 +154,10 @@ interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// opens once the binding exists. A clone that resolves once during /// `initialize` and stores the answer is unaffected by any later /// rebinding. - /// - Registry-resolved authority is now the ordinary case rather than a - /// special one: it is simply `data` that names things instead of naming - /// addresses. `data` MAY be empty, and an implementation that resolves - /// everything from the registry will pass empty `data`. + /// - Registry-resolved authority is the ordinary case: it is simply `data` + /// that names things instead of naming addresses. `data` MAY be empty, and + /// an implementation that resolves everything from the registry will pass + /// empty `data`. /// /// # Obligation on the factory, not on the consumer /// @@ -183,8 +181,7 @@ interface ICloneableFactoryV4 is ICloneableFactoryV3 { /// caller here chooses only words 1 and 2 (`salt` and `keccak256(data)`); /// neither can place the other derivation's tag in word 0, so neither can /// aim its entry point at an address the other produces. The disjointness is - /// a property of the two fixed tags — not of a preimage length an attacker - /// might match or a value an attacker might fail to reach. + /// a property of the two fixed tags. /// /// # Events ///