feat: open-salt deterministic clone variant (ICloneableFactoryV4) - #51
feat: open-salt deterministic clone variant (ICloneableFactoryV4)#51thedavidmeister wants to merge 2 commits into
Conversation
…ation) 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) <noreply@anthropic.com>
Walkthrough
ChangesOpen-salt deterministic cloning
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant CloneFactory
participant Implementation
Caller->>CloneFactory: cloneDeterministicOpenSalt(implementation, data, salt)
CloneFactory->>Implementation: Deploy clone with verbatim salt
CloneFactory->>Implementation: Initialize clone with data
CloneFactory-->>Caller: Return clone address
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 16-20: Update the cloneDeterministicOpenSalt documentation to make
cross-chain address portability conditional: the same raw salt yields the same
address only when both the factory and implementation addresses match across
chains, since CREATE2 incorporates the factory and the EIP-1167 initialization
code incorporates the implementation. Preserve the existing security guidance
about caller-controlled authority.
In `@test/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol`:
- Around line 123-135: Update _containsSelector so it verifies the selector is
reachable through executable dispatcher logic rather than merely matching bytes
anywhere in code. Decode the runtime dispatcher, or deploy the frozen runtime
and invoke each expected selector with valid arguments, and preserve the test’s
failure behavior when an entry point is unavailable.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 44e6cc07-f166-4b4d-808b-56418e7422a5
⛔ Files ignored due to path filters (1)
src/generated/0_1_6/CloneFactory.pointers.solis excluded by!**/generated/**
📒 Files selected for processing (7)
CLAUDE.mdREADME.mdsrc/concrete/CloneFactory.solsrc/interface/ICloneableFactoryV4.solsrc/lib/LibCloneFactoryDeploy.soltest/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.soltest/src/lib/LibCloneFactoryDeployTaggedConstants.t.sol
… dispatch 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) <noreply@anthropic.com>
|
🤖 ai:producer |
Closes #50
What
CloneFactorygains a second deterministic entry point whoseCREATE2salt isthe caller-supplied salt verbatim, so the clone address is
CREATE2(factory, salt, EIP1167(impl))— no identity in the derivation:cloneDeterministicOpenSalt(address implementation, bytes data, bytes32 salt)predictDeterministicAddressOpenSalt(address implementation, bytes32 salt)cloneDeterministicandpredictDeterministicAddressare untouched. Theirmsg.sendernamespacing is a guarantee other consumers rely on, so this ispurely additive and the two derivations are provably disjoint (a fuzz test
asserts no
(implementation, salt, deployer)maps to the same address underboth).
_requireImplementationCodeand_initializeCloneare reused as-is, soclone-and-initialize stays atomic and the open variant's failure modes are
identical to the existing one.
Interface versioning
ICloneableFactoryV3is published, so the new functions go on a newICloneableFactoryV4. It extendsICloneableFactoryV3rather thanrestating it: the repo's stated reason for
V3being standalone is that thenon-deterministic
clone()was intentionally dropped fromV2. Nothing isdropped here, so inheriting is the shape that matches the documented rule.
CloneFactorynow declaresis ICloneableFactoryV4; the@inheritdoctags onthe two pre-existing functions still name
V3, because that is where they aredeclared.
The footgun, and the docs that are the deliverable
With no
msg.senderin the salt, anyone can land on the address with their owndata. Initialization is atomic and runs once, so the first mover sets thecontract's authority permanently and there is no recovery — the address is
occupied and nobody can redeploy over it.
The NatSpec on
cloneDeterministicOpenSaltstates the qualifying condition assomething a reader can actually check against their own implementation, not as a
vague warning:
with the concrete consequence spelled out (
initializemust not read anyaddress, key, role, owner or admin out of
data; aninitializetaking anownerargument is disqualified by that argument alone), the reasonpermissionless deploys are otherwise harmless (a Zoltu deploy has no arguments,
so a front-runner produces byte-for-byte the intended contract), and the
intended pairing that reaches that same position with arguments — an address
registry (rainlanguage/rain.deploy#25) where
initializeresolves the admin byname and the salt commits to that name, so a squatter can neither
substitute a different admin (the registry decides) nor pass a different name (a
different name is a different address). The NatSpec also notes that either half
alone is insufficient.
README.mdandCLAUDE.mdare updated for the new interface; both were alsostill describing
CloneFactoryas implementingICloneableFactoryV2, which wasalready stale.
Release / deploy
Consumers bump to soldeer
rain-factory0.1.6.[package].versioninfoundry.tomlis the next in-development version and0.1.5is the latest onthe registry, so autopublish on merge publishes exactly
0.1.6.Per the
package-release.yamlcontract ("a version's deploy-pin snapshot isbuilt by the PR that changes the bytecode"), this PR runs
forge script script/BuildPointers.soland commitssrc/generated/0_1_6/CloneFactory.pointers.solplus the regeneratedLibCloneFactoryDeploy. Existing frozen snapshots are untouched (append-only).LibCloneFactoryDeployProd.t.solis red on all five networks —CloneFactory not deployed, because 0.1.6's bytecode is not on chain yet.0.1.6 deploys to
0x19272bCcFcb032eaC545E74ADFa168fDeD3e8d83, codehash0x1a16009998834f07d5ccab032c39377f6528870eec5abd47817f9467187b4012.Everything else in
rainix-sol / testpasses (29/29), andstaticandlegalare green.This is not fixable on the branch by code, and it is the exact shape that says
the repo is still on the legacy single-current-pin lifecycle: autopublish
on merge, plus one
LibCloneFactoryDeploy"current" pin that a bytecodechange immediately invalidates until a deploy catches up. Under the
deploy/library split, deploys do not block merges at all — a library repo does
not carry a live-chain pin, and a concrete's per-tag records are frozen and
tag-released with the deploy as a decoupled manual dispatch. So the right fix
is migrating this repo to that split, not branch-deploy choreography to paint
the pin green. I have deliberately not dispatched
Manual sol artifactshere.A deploy would go green — CREATE2 is idempotent and permissionless, so it is
cheap either way — but it would be paying the retired choreography rather than
the standing one. Say which you want.
QA
Category check. Solidity contract change, additive external functions on a
deployed-and-pinned concrete. Categories that apply: address-derivation
correctness, atomicity of clone+initialize, non-regression of the existing
sender-namespacing guarantee, and deploy-pin consistency. Not applicable:
storage layout (the factory is stateless), upgrade/migration (clones are
immutable EIP1167 proxies), access control (the factory is permissionless by
design and that is the subject of the change, not an omission).
Oracle. The spec is issue #50 plus EIP-1167 +
CREATE2, not theimplementation. Predicted addresses are asserted against OpenZeppelin's own
Clones.predictDeterministicAddressunder an independently constructed salt(raw
saltfor the open variant,keccak256(abi.encode(deployer, salt))for thenamespaced one), so the tests do not simply restate
CloneFactory's arithmeticback to itself. Caller-independence is proven by actually deploying twice from
two different senders with
vm.snapshotState()/vm.revertToState()betweenthem and comparing the two deployed addresses — not by comparing two
predictions, which would only test the prediction function.
Discriminating tests (new,
test/src/concrete/CloneFactoryCloneDeterministicOpenSalt.t.sol, 11 tests, 2048 fuzz runs each):…SaltIsVerbatim…MatchesPredictdata…CallerIndependent(impl, salt)from the same state land on the SAME address…PredictCallerIndependent…DiffersFromSenderNamespaced(impl, salt, deployer)…DoesNotConsumeNamespacedSalt…ManyClonesPerImpl…SecondDeployRevertsErrors.FailedDeployment— it does not silently return the existing clone — and the first clone's state is untouched…EventNewClone(sender, impl, child, salt, data)with the raw salt…InitializeFailureFailsInitializationFailed, and the address is left with zero code (atomicity)…ZeroImplementationCodeSizeZeroImplementationCodeSizePlus
testCloneFactory_0_1_6_DeployedBytecodeServesBothEntryPointsin thetagged-constants suite: the frozen 0.1.6
CREATION_CODEis Zoltu-deployed andall four entry points are called on the result through the
ICloneableFactoryV4ABI, so a snapshot pinning an address for bytecode that does not serve
cloneDeterministicOpenSaltcannot pass silently. Discrimination verified byswapping the pinned constant to
CLONE_FACTORY_CREATION_CODE_0_1_5(which hasno open-salt entry point):
EvmError: Revert.The existing
CloneFactoryCloneDeterministic.t.solalready pins the namespacedderivation (
testCloneDeterministicSaltIsAbiEncodeHash) and its sender-scoping(
testCloneDeterministicSenderScoped); the mutation run below confirms bothstill kill, so they are left as-is rather than duplicated.
Mutations applied, with the killing test. Each mutation was applied to the
working tree, the whole suite run, then the tree restored. Every mutant is
listed with a behavioural killer — the codehash pin (
testDeployAddress/testExpectedCodeHash) also trips on all eight, but that is a bytecode oracle,not evidence of behavioural coverage, so it is excluded here.
msg.sender…CallerIndependent,…MatchesPredict,…DoesNotConsumeNamespacedSalt,…SecondDeployReverts…SaltIsVerbatim,…MatchesPredict,…PredictCallerIndependent,…CallerIndependent_requireImplementationCode…ZeroImplementationCodeSize(reverts, but with no data — the selector assertion discriminates)_initializeClone…MatchesPredict,…Event,…InitializeFailureFails,…SecondDeployRevertsinitializereturn code left unchecked…InitializeFailureFails+ existingtestCloneDeterministicInitializeFailureFailscloneDeterministicdrops its sender namespacingtestCloneDeterministicSenderScoped+ new…DoesNotConsumeNamespacedSaltpredictDeterministicAddressdrops its namespacingtestCloneDeterministicSaltIsAbiEncodeHash,testCloneDeterministicSenderScoped,testCloneDeterministicMatchesPredict+ new…DiffersFromSenderNamespacedNewCloneemits the effective salt instead of the raw salt…Event+ existingtestCloneDeterministicEventMutants 6 and 7 are the ones that matter for "do not break the existing
guarantee": the suite fails loudly if the sender-namespacing is ever removed.
CodeRabbit. Two findings, both correct, both fixed in
f4635ceand thethreads replied to and resolved: (1) cross-chain address portability is
conditional on the factory AND the implementation being at the same address on
each chain — my
READMEandICloneableFactoryV4NatSpec both claimed itunconditionally, now corrected in both; (2) the entry-point check was a byte
scan of the runtime code, which a selector in constant data passes without being
dispatchable — replaced with the real deploy-and-call test described above.
Gates run locally (rainix
sol-shell, the same commands the CI reusablesinvoke):
forge test— 29 passed, 0 failed (fork-onlyLibCloneFactoryDeployProd.t.solexcluded, see Release/deploy above);forge fmt --checkclean;slither .— 13 contracts, 97 detectors, 0 results;reuse lintcompliant;rainix-sol-single-contractclean.Underspecified, decided rather than guessed
NewCloneevent does not distinguish the two entry points. Reusing_initializeCloneas-is (as the issue requires) means both variants emit theidentical
NewClone, so an indexer cannot tell from the event alone whichderivation produced the address. This is harmless in practice because the
event carries the
cloneaddress explicitly, and a separate event would havemeant not reusing
_initializeClone. Documented inICloneableFactoryV4andasserted in
…Eventrather than left implicit. Say the word if a distinctevent is wanted instead.
cloneDeterministicOpenSalt/predictDeterministicAddressOpenSalt— long, but consistent suffixes on theexisting names, and they read at the call site as the variant they are.
🤖 Generated with Claude Code