Skip to content

Commit 9a73b93

Browse files
committed
feat: open-salt deterministic clone variant (ICloneableFactoryV4)
The deploy half of rainlanguage/rain.factory#51, which was written against the pre-split rain.factory tree. Five of that PR's eight files targeted paths rain.factory#47 deleted; they land here instead. CloneFactory gains 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 implementation, bytes data, bytes32 salt) predictDeterministicAddressOpenSalt(address implementation, bytes32 salt) cloneDeterministic and predictDeterministicAddress are untouched: their msg.sender namespacing is a guarantee other consumers rely on, so this is purely additive and the two derivations are provably disjoint (a fuzz test asserts no (implementation, salt, deployer) maps to the same address under both). _requireImplementationCode and _initializeClone are reused as-is, so clone-and-initialize stays atomic and the open variant's failure modes are identical to the existing one. The rain-factory Soldeer pin moves 0.1.5 -> 0.1.7 and every versioned import prefix moves with it, because ICloneableFactoryV4 only exists from 0.1.7. That version is not published until rain.factory#51 merges, so this branch cannot resolve its dependencies until then. No hand-authored src/generated/0_1_6/ dir is carried over. Under the rolling candidate model this branch is stacked on, the snapshot that describes current source is src/generated/candidate/, regenerated by BuildPointers; a numbered dir is frozen only by script/cut-release.sh at tag time. candidate/ is regenerated here and lands on the same address 0x19272bCcFcb032eaC545E74ADFa168fDeD3e8d83 and codehash 0x1a16009998834f07d5ccab032c39377f6528870eec5abd47817f9467187b4012 that #51's hand-authored 0_1_6 dir recorded. The frozen-snapshot dirs 0_1_3, 0_1_4 and 0_1_5 are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a4548f9 commit 9a73b93

11 files changed

Lines changed: 362 additions & 31 deletions

‎.gas-snapshot‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
1-
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 421457, ~: 371564)
2-
CloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 161266, ~: 161266)
3-
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 569724, ~: 482275)
4-
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 423089, ~: 372826)
5-
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4686, ~: 4686)
6-
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 585442, ~: 485815)
7-
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10699, ~: 10678)
8-
LibCloneFactoryDeployCandidateTest:testCandidateCreationDeploysToPinnedAddress() (gas: 316633)
1+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 582462, ~: 439968)
2+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes32,address) (runs: 2048, μ: 5400, ~: 5400)
3+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 574640, ~: 436159)
4+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 418423, ~: 349063)
5+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 164888, ~: 164888)
6+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 568887, ~: 437277)
7+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 419757, ~: 349902)
8+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes32,address,address) (runs: 2048, μ: 8766, ~: 8766)
9+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSaltIsVerbatim(address,bytes32) (runs: 2048, μ: 4262, ~: 4262)
10+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,bytes,address,address) (runs: 2048, μ: 1040442091, ~: 1040442127)
11+
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10701, ~: 10678)
12+
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 418483, ~: 349123)
13+
CloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 161288, ~: 161288)
14+
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 569073, ~: 437463)
15+
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 420117, ~: 350262)
16+
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4708, ~: 4708)
17+
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 583541, ~: 441047)
18+
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10717, ~: 10700)
19+
LibCloneFactoryDeployCandidateTest:testCandidateCreationDeploysToPinnedAddress() (gas: 339992)
20+
LibCloneFactoryDeployCandidateTest:testCandidateDeployedBytecodeServesBothEntryPoints() (gas: 733209)
921
LibCloneFactoryDeployCandidateTest:testCandidateIsTheAliasedSnapshot() (gas: 3708)
10-
LibCloneFactoryDeployCandidateTest:testCandidateRuntimeHashesToBytecodeHash() (gas: 879)
11-
LibCloneFactoryDeployCandidateTest:testCandidateSelfConsistent() (gas: 4706)
22+
LibCloneFactoryDeployCandidateTest:testCandidateRuntimeHashesToBytecodeHash() (gas: 928)
23+
LibCloneFactoryDeployCandidateTest:testCandidateSelfConsistent() (gas: 4791)
1224
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_CreationDeploysToPinnedAddress() (gas: 240145)
1325
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_RuntimeHashesToBytecodeHash() (gas: 755)
1426
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_4_CreationDeploysToPinnedAddress() (gas: 240146)
1527
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_4_RuntimeHashesToBytecodeHash() (gas: 755)
1628
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_5_CreationDeploysToPinnedAddress() (gas: 316590)
1729
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_5_RuntimeHashesToBytecodeHash() (gas: 922)
18-
LibCloneFactoryDeployTest:testDeployAddress() (gas: 315988)
19-
LibCloneFactoryDeployTest:testExpectedCodeHash() (gas: 311566)
30+
LibCloneFactoryDeployTest:testDeployAddress() (gas: 339275)
31+
LibCloneFactoryDeployTest:testExpectedCodeHash() (gas: 334816)

‎CLAUDE.md‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,23 @@ as the `rain-factory` Soldeer dependency, so they are read under
6868
`dependencies/rain-factory-<version>/src/interface/`.
6969

7070
- `src/concrete/CloneFactory.sol` — The single concrete implementation of
71-
`ICloneableFactoryV3`. Uses OpenZeppelin `Clones.cloneDeterministic()`; there
72-
is no plain `clone()`.
71+
`ICloneableFactoryV4`. Uses OpenZeppelin `Clones.cloneDeterministic()` for
72+
both deterministic entry points; there is no plain `clone()`. The two entry
73+
points differ ONLY in the salt they pass to `Clones`:
74+
- `cloneDeterministic` / `predictDeterministicAddress` (declared on
75+
`ICloneableFactoryV3`, which V4 extends) namespace the caller-supplied salt
76+
by `msg.sender` via `_effectiveSalt`, so a caller's `(implementation, salt)`
77+
address cannot be reached by another account.
78+
- `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt` pass
79+
the caller-supplied salt verbatim, so the address carries no identity and
80+
anyone can deploy it. Only safe for implementations whose `initialize`
81+
takes no caller-controlled authority — the NatSpec on
82+
`ICloneableFactoryV4.cloneDeterministicOpenSalt` is the spec for that
83+
condition, and it lives in rain.factory, not here.
84+
85+
Both share `_requireImplementationCode` and `_initializeClone`, so
86+
clone-and-initialize is atomic and the failure modes are identical across the
87+
two.
7388
- `src/lib/LibCloneFactoryDeploy.sol` — Deterministic deployment address and
7489
codehash constants (generated; aliases the rolling `src/generated/candidate/`
7590
snapshot).

‎README.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,31 @@ here as the `rain-factory` Soldeer package. Consumers that need only the
1010
interfaces depend on `rain-factory`; consumers that need the deployed
1111
address/codehash pins depend on `rain-factory-deploy`.
1212

13+
## Entry points
14+
15+
`CloneFactory` implements `ICloneableFactoryV4`, letting any compatible
16+
`ICloneableV2` contract be cloned as an EIP1167 proxy and initialized
17+
atomically. It offers two deterministic (`CREATE2`) entry points that differ
18+
only in how the salt is derived:
19+
20+
- `cloneDeterministic` namespaces the caller-supplied salt by `msg.sender`, so
21+
nobody else can reach the caller's address — but the deploying account is
22+
baked into that address forever.
23+
- `cloneDeterministicOpenSalt` uses the caller-supplied salt verbatim, so the
24+
address is a function of `(implementation, salt)` and the factory alone: every
25+
account reaches the same address, and so can anyone. That also makes it the
26+
same address across chains, but only where both the factory and the
27+
implementation are themselves at the same address on each chain — `CREATE2`
28+
hashes the factory, and the EIP1167 creation code it hashes contains the
29+
implementation.
30+
31+
Open-salt is ONLY safe for implementations whose `initialize` takes no
32+
caller-controlled authority: clone-and-initialize is atomic and runs once, so
33+
the first deployer's `data` sets the clone's authority permanently, with no
34+
recovery. Read the NatSpec on
35+
`ICloneableFactoryV4.cloneDeterministicOpenSalt` (in `rain.factory`) before
36+
using it — the qualifying condition is stated there, not here.
37+
1338
## Snapshots
1439

1540
`src/generated/` holds two kinds of deploy-pin snapshot, both with the same file

‎foundry.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ forge-std = "1.16.1"
4040
"rain-extrospection" = "0.1.1"
4141
"rain-deploy" = "0.1.3"
4242
"rain-sol-codegen" = "0.1.0"
43-
"rain-factory" = "0.1.5"
43+
"rain-factory" = "0.1.7"
4444

4545
[soldeer]
4646
recursive_deps = false

‎src/concrete/CloneFactory.sol‎

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
33
pragma solidity =0.8.25;
44

5-
import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "rain-factory-0.1.5/src/interface/ICloneableV2.sol";
6-
import {ICloneableFactoryV3} from "rain-factory-0.1.5/src/interface/ICloneableFactoryV3.sol";
5+
import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "rain-factory-0.1.7/src/interface/ICloneableV2.sol";
6+
// `ICloneableFactoryV3` is imported for the `@inheritdoc` references on the
7+
// functions it declares; `ICloneableFactoryV4` inherits rather than redeclares
8+
// them, so the tag must name V3 and V3 must be in scope here.
9+
import {ICloneableFactoryV3} from "rain-factory-0.1.7/src/interface/ICloneableFactoryV3.sol";
10+
import {ICloneableFactoryV4} from "rain-factory-0.1.7/src/interface/ICloneableFactoryV4.sol";
711
import {Clones} from "@openzeppelin-contracts-5.6.1/proxy/Clones.sol";
812

913
/// Thrown when an implementation has zero code size which is always a mistake.
@@ -13,15 +17,21 @@ error ZeroImplementationCodeSize();
1317
error InitializationFailed();
1418

1519
/// @title CloneFactory
16-
/// @notice A fairly minimal implementation of `ICloneableFactoryV3` that uses
20+
/// @notice A fairly minimal implementation of `ICloneableFactoryV4` that uses
1721
/// Open Zeppelin `Clones` to create EIP1167 clones of a reference bytecode. The
1822
/// reference bytecode MUST implement `ICloneableV2`.
1923
///
20-
/// `cloneDeterministic` deploys via `CREATE2` at a pre-computable address
21-
/// (`predictDeterministicAddress`), namespacing the caller-supplied salt by
22-
/// `msg.sender` so a caller's `(implementation, salt)` address cannot be squatted
23-
/// by another account.
24-
contract CloneFactory is ICloneableFactoryV3 {
24+
/// Two deterministic entry points, both `CREATE2`, differing only in the salt:
25+
///
26+
/// - `cloneDeterministic` / `predictDeterministicAddress` namespace the
27+
/// caller-supplied salt by `msg.sender` (see `_effectiveSalt`) so a caller's
28+
/// `(implementation, salt)` address cannot be squatted by another account.
29+
/// - `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt` use
30+
/// the caller-supplied salt verbatim, so the address carries no identity and
31+
/// anyone can deploy it. This is only safe for implementations whose
32+
/// `initialize` takes no caller-controlled authority — read the warning on
33+
/// `ICloneableFactoryV4.cloneDeterministicOpenSalt` before using it.
34+
contract CloneFactory is ICloneableFactoryV4 {
2535
/// @inheritdoc ICloneableFactoryV3
2636
function cloneDeterministic(address implementation, bytes calldata data, bytes32 salt) external returns (address) {
2737
_requireImplementationCode(implementation);
@@ -39,6 +49,24 @@ contract CloneFactory is ICloneableFactoryV3 {
3949
return Clones.predictDeterministicAddress(implementation, _effectiveSalt(deployer, salt), address(this));
4050
}
4151

52+
/// @inheritdoc ICloneableFactoryV4
53+
function cloneDeterministicOpenSalt(address implementation, bytes calldata data, bytes32 salt)
54+
external
55+
returns (address)
56+
{
57+
_requireImplementationCode(implementation);
58+
// CREATE2 clone at the caller-supplied salt verbatim: no `_effectiveSalt`
59+
// namespacing, so the address is the same for every caller and there is
60+
// no identity in the derivation.
61+
address child = Clones.cloneDeterministic(implementation, salt);
62+
return _initializeClone(implementation, child, data, salt);
63+
}
64+
65+
/// @inheritdoc ICloneableFactoryV4
66+
function predictDeterministicAddressOpenSalt(address implementation, bytes32 salt) external view returns (address) {
67+
return Clones.predictDeterministicAddress(implementation, salt, address(this));
68+
}
69+
4270
/// @dev The CREATE2 salt actually used: the caller-supplied `salt` namespaced
4371
/// by the deploying account. Prevents a caller's `(implementation, salt)`
4472
/// address being front-run/squatted by another account, while still letting a
@@ -69,7 +97,7 @@ contract CloneFactory is ICloneableFactoryV3 {
6997
{
7098
emit NewClone(msg.sender, implementation, child, salt, data);
7199
// Checking the return value of initialize is mandatory as per
72-
// ICloneableFactoryV3.
100+
// ICloneableFactoryV3 and ICloneableFactoryV4.
73101
if (ICloneableV2(child).initialize(data) != ICLONEABLE_V2_SUCCESS) {
74102
revert InitializationFailed();
75103
}

0 commit comments

Comments
 (0)