Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 421457, ~: 371564)
CloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 161266, ~: 161266)
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 569724, ~: 482275)
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 423089, ~: 372826)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4686, ~: 4686)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 585442, ~: 485815)
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10699, ~: 10678)
LibCloneFactoryDeployCandidateTest:testCandidateCreationDeploysToPinnedAddress() (gas: 316633)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltCallerIndependent(bytes32,bytes,address,address) (runs: 2048, μ: 588853, ~: 486442)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDataInDerivation(bytes32,bytes,bytes) (runs: 2048, μ: 578320, ~: 471101)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDiffersFromSenderNamespaced(address,bytes,bytes32,bytes32,address) (runs: 2048, μ: 6748, ~: 6672)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDisjointFromNamespacedAtLeftPaddedAddressSalt(address,bytes,bytes) (runs: 2048, μ: 572194, ~: 470240)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt(bytes32,bytes) (runs: 2048, μ: 577796, ~: 436545)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltEvent(bytes32,bytes) (runs: 2048, μ: 420167, ~: 349404)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 166138, ~: 166138)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltIsDomainTaggedHash(address,bytes,bytes32) (runs: 2048, μ: 5883, ~: 5792)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 564711, ~: 438028)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltMatchesPredict(bytes32,bytes) (runs: 2048, μ: 422441, ~: 351127)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltPredictCallerIndependent(address,bytes,bytes32,address,address) (runs: 2048, μ: 10997, ~: 10918)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltSecondDeployReverts(bytes32,bytes,address,address) (runs: 2048, μ: 1040444140, ~: 1040443099)
CloneFactoryCloneDeterministicOpenSaltTest:testCloneDeterministicOpenSaltZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10659, ~: 10634)
CloneFactoryCloneDeterministicTest:testCloneDeterministicEvent(bytes32,bytes) (runs: 2048, μ: 419856, ~: 349123)
CloneFactoryCloneDeterministicTest:testCloneDeterministicInitializeFailureFails(bytes32,bytes32) (runs: 2048, μ: 161288, ~: 161288)
CloneFactoryCloneDeterministicTest:testCloneDeterministicManyClonesPerImpl(bytes32,bytes32,bytes) (runs: 2048, μ: 564092, ~: 437463)
CloneFactoryCloneDeterministicTest:testCloneDeterministicMatchesPredict(bytes32,bytes) (runs: 2048, μ: 421499, ~: 350262)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSaltIsAbiEncodeHash(address,bytes32,address) (runs: 2048, μ: 4708, ~: 4708)
CloneFactoryCloneDeterministicTest:testCloneDeterministicSenderScoped(bytes32,bytes,address,address) (runs: 2048, μ: 588232, ~: 485903)
CloneFactoryCloneDeterministicTest:testCloneDeterministicZeroImplementationCodeSize(address,bytes,bytes32) (runs: 2048, μ: 10715, ~: 10700)
LibCloneFactoryDeployCandidateTest:testCandidateCreationDeploysToPinnedAddress() (gas: 366569)
LibCloneFactoryDeployCandidateTest:testCandidateDeployedBytecodeServesBothEntryPoints() (gas: 760962)
LibCloneFactoryDeployCandidateTest:testCandidateIsTheAliasedSnapshot() (gas: 3708)
LibCloneFactoryDeployCandidateTest:testCandidateRuntimeHashesToBytecodeHash() (gas: 879)
LibCloneFactoryDeployCandidateTest:testCandidateSelfConsistent() (gas: 4706)
LibCloneFactoryDeployCandidateTest:testCandidateRuntimeHashesToBytecodeHash() (gas: 977)
LibCloneFactoryDeployCandidateTest:testCandidateSelfConsistent() (gas: 4924)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_CreationDeploysToPinnedAddress() (gas: 240145)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_3_RuntimeHashesToBytecodeHash() (gas: 755)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_4_CreationDeploysToPinnedAddress() (gas: 240146)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_4_RuntimeHashesToBytecodeHash() (gas: 755)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_5_CreationDeploysToPinnedAddress() (gas: 316590)
LibCloneFactoryDeployTaggedConstantsTest:testCloneFactory_0_1_5_RuntimeHashesToBytecodeHash() (gas: 922)
LibCloneFactoryDeployTest:testDeployAddress() (gas: 315988)
LibCloneFactoryDeployTest:testExpectedCodeHash() (gas: 311566)
LibCloneFactoryDeployTest:testDeployAddress() (gas: 365802)
LibCloneFactoryDeployTest:testExpectedCodeHash() (gas: 361282)
34 changes: 32 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,38 @@ as the `rain-factory` Soldeer dependency, so they are read under
`dependencies/rain-factory-<version>/src/interface/`.

- `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 deterministic entry points; there is no plain `clone()`. The two entry
points differ ONLY in the salt they pass to `Clones`:
- `cloneDeterministic` / `predictDeterministicAddress` (declared on
`ICloneableFactoryV3`, which V4 extends) namespace the caller-supplied salt
by `msg.sender` via `_effectiveSalt`, so a caller's `(implementation, salt)`
address cannot be reached by another account. `data` is outside that
derivation.
- `cloneDeterministicOpenSalt` / `predictDeterministicAddressOpenSalt` derive
the salt via `_effectiveOpenSalt` as
`keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`,
so the address carries no identity and anyone can deploy it, but everyone
who does deploys the same contract initialized with the same bytes.
`predictDeterministicAddressOpenSalt` therefore takes `data` — it is one of
the derivation's inputs. The residual condition on implementations
(`initialize` MUST NOT read `tx.origin`) is specified by the NatSpec on
`ICloneableFactoryV4.cloneDeterministicOpenSalt`, which lives in
rain.factory, not here.

Both share `_requireImplementationCode` and `_initializeClone`, so
clone-and-initialize is atomic and the failure modes are identical across the
two.

**The two salt derivations MUST have disjoint images**, and `CloneFactory` is
where `ICloneableFactoryV4`'s MUST NOT on the factory is actually held: 96
bytes led by the domain constant versus 64 bytes led by a left-padded address.
Drop the domain word and any account `A` reaches every open-salt address whose
`salt` equals `bytes32(uint256(uint160(A)))` via `cloneDeterministic` with
arbitrary `data`. Do not add a third entry point that hashes to either shape,
and do not change the shape of either preimage.
`testCloneDeterministicOpenSaltDisjointFromNamespacedAtLeftPaddedAddressSalt`
is the gate.
- `src/lib/LibCloneFactoryDeploy.sol` — Deterministic deployment address and
codehash constants (generated; aliases the rolling `src/generated/candidate/`
snapshot).
Expand Down
83 changes: 70 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,62 @@ here as the `rain-factory` 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`.

## Entry points

`CloneFactory` implements `ICloneableFactoryV4`, letting any compatible
`ICloneableV2` contract be cloned as an EIP1167 proxy and initialized
atomically. 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
the address commits to WHO deployed: nobody else can reach the caller's
address, but the deploying account is baked into it forever and `data` is
outside the derivation.
- `cloneDeterministicOpenSalt` derives the salt as
`keccak256(abi.encode(ICLONEABLE_FACTORY_V4_OPEN_SALT_DOMAIN, salt, keccak256(data)))`
— the domain constant being
`keccak256("ICloneableFactoryV4.cloneDeterministicOpenSalt")`, declared in
`rain.factory` so third parties recompute the address rather than trust it —
so the address commits to WHAT was deployed and to nothing about who deployed
it: it is a function of `(factory, implementation, salt, data)` alone. Every
account reaches the same address, and 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.

Because `data` is in the derivation, open-salt needs no per-implementation audit
of what a squatter could pass. A front-runner who passes different `data`
derives a different address and has deployed their own contract at their own
expense; one who passes the same `data` has deployed exactly the intended
contract with the intended bytes and has paid the gas for it. What is left is
that the address cannot fix what `initialize` reads that is not `data`, so an
implementation used this way MUST NOT read `tx.origin`. The full statement of
that condition and of the residual timing lever is the NatSpec on
`ICloneableFactoryV4.cloneDeterministicOpenSalt` (in `rain.factory`), not here.
The cost open-salt does carry is that the address is not knowable until `data`
is final, and a consumer pinning one must be able to reproduce those bytes
exactly, ABI encoding and all.

### The domain separator is load-bearing

`ICloneableFactoryV4` states the disjointness of the two derivations as a MUST
NOT on the **factory**: no other entry point may `CREATE2` in the open-salt
image with caller-supplied `data`. `cloneDeterministic` is exactly such an entry
point, so `CloneFactory` holds the rule structurally — a 96-byte preimage led by
the domain constant against a 64-byte preimage led by a left-padded address.

Without the domain word both preimages would be 64 bytes led by a word the
caller chooses, and since `abi.encode` left-pads an address into the same word a
`bytes32` salt already is, any account `A` would reach every open-salt address
whose `salt` equals `bytes32(uint256(uint160(A)))` by calling
`cloneDeterministic(implementation, evilData, keccak256(data))` — a choice of
salt, not a preimage search.
`testCloneDeterministicOpenSaltDisjointFromNamespacedAtLeftPaddedAddressSalt` is
the test that fails if that ever stops holding: it builds that exact squat,
asserts against the factory's own namespaced prediction that an untagged
derivation would land on it, and then shows the real one does not.

## Snapshots

`src/generated/` holds two kinds of deploy-pin snapshot, both with the same file
Expand Down Expand Up @@ -47,19 +103,20 @@ revisions.

`sol-v0.1.6` exists as a tag on `685bb2ba`. Its `rainix-tag-release` run
([30097157490](https://github.com/rainlanguage/rain.factory.deploy/actions/runs/30097157490))
got as far as *Verify live chain matches the fresh pins* and died there — all
five fork tests failed with `vm.createSelectFork: environment variable
<NETWORK>_RPC_URL not found`. The reusable exported the fork endpoints under the
**secret** names (`RPC_URL_<NETWORK>_FORK`), while `[rpc_endpoints]` in
`foundry.toml` reads `${<NETWORK>_RPC_URL}`, so every endpoint resolved to an
empty string. Publish, commit-back and GitHub Release were all skipped, which is
why the tag exists with no revision, no release and no `0_1_6` snapshot behind
it.
got as far as _Verify live chain matches the fresh pins_ and died there — all
five fork tests failed with
`vm.createSelectFork: environment variable
<NETWORK>_RPC_URL not found`. The
reusable exported the fork endpoints under the **secret** names
(`RPC_URL_<NETWORK>_FORK`), while `[rpc_endpoints]` in `foundry.toml` reads
`${<NETWORK>_RPC_URL}`, so every endpoint resolved to an empty string. Publish,
commit-back and GitHub Release were all skipped, which is why the tag exists
with no revision, no release and no `0_1_6` snapshot behind it.

That was a defect in `rainix-tag-release`, not in this repo, and it is fixed
upstream: `rainix` now runs an `rpc-preflight` step that binds each env name
foundry actually reads to an endpoint probed healthy at that moment. The next tag
does not hit this.
foundry actually reads to an endpoint probed healthy at that moment. The next
tag does not hit this.

Two consequences for whoever cuts the first release:

Expand All @@ -70,8 +127,8 @@ Two consequences for whoever cuts the first release:
- **The fork RPCs still gate the release.** The verify step is the repo's own
fork suite, so a release only publishes if the pins resolve on every supported
chain. Those endpoints are currently intermittent (a free-plan `lb.drpc.live`
returning quota and 408 errors), which reds the same suite on ordinary PRs. Get
them healthy before tagging: a transient failure here fails the release, and
the fix is to tag again, not to retry the run.
returning quota and 408 errors), which reds the same suite on ordinary PRs.
Get them healthy before tagging: a transient failure here fails the release,
and the fix is to tag again, not to retry the run.

See rainlanguage/rain.factory#46 for the split rationale.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ forge-std = "1.16.1"
"rain-extrospection" = "0.1.1"
"rain-deploy" = "0.1.3"
"rain-sol-codegen" = "0.1.0"
"rain-factory" = "0.1.5"
"rain-factory" = "0.1.7"

[soldeer]
recursive_deps = false
Expand Down
Loading
Loading