Skip to content

AMT coverage: g3-libicloneablefactoryv4-clone - #77

Open
thedavidmeister wants to merge 7 commits into
mainfrom
2026-08-24-amt-g3-libicloneablefactoryv4-clone
Open

AMT coverage: g3-libicloneablefactoryv4-clone#77
thedavidmeister wants to merge 7 commits into
mainfrom
2026-08-24-amt-g3-libicloneablefactoryv4-clone

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Note

Updated after the fixture consolidation — everything below is the original record and is left as written.

Two fixtures it names no longer exist. The claims they carried are all still made and still pass; the fixtures behind them changed:

Named below Now
TestCloneableLiteralSuccess TestCloneable, which now hard-codes the literal keccak256("ICloneableV2.initialize") itself rather than importing ICLONEABLE_V2_SUCCESS. The lockstep-breaking property moved to THE conforming fixture, so every flow test in the suite has it instead of two.
TestCloneableNearMissSuccess TestCloneableFailure plus abi.encode(keccak256("ICloneableV2.initialise")) at the call site. It returns whatever bytes32 its data decodes to, so the near miss is now named where it is used instead of buried in a fixture.

Mutation-checked after the swap, not assumed: with initialize(data) != ICLONEABLE_V2_SUCCESS mutated to == bytes32(0), testCloneableV2SuccessNearMissIsRejected still fails.

Paths under test/src/concrete/ below are now test/concrete/test/src/** mirrors src/**, and fixtures have no src counterpart.


Adversarial mutation-testing pass over group g3-libicloneablefactoryv4-clone — the four LibICloneableFactoryV4 clone entry points, the two ICloneableFactoryV4 domain tags, and the ICLONEABLE_V2_SUCCESS sentinel. 29 behaviours.

Scanned commit c1c2afd.

What changed

No file under src/ is touched.

The gap that mattered

ICLONEABLE_V2_SUCCESS had zero direct coverage. TestCloneable imports the same constant the library compares against, so both sides of that comparison moved in lockstep — mutating the literal string left all 42 tests green. Confirmed, not assumed:

F1 ICLONEABLE_V2_SUCCESS string changed by one character: SURVIVED
F2 ICLONEABLE_V2_SUCCESS is a different word entirely:    SURVIVED

This is a value third parties reproduce — ICloneableV2 tells every implementer to return keccak256("ICloneableV2.initialize") — so it is consensus-critical in the same way the two domain tags are, and it is now pinned the same way testDomainTagsPinned pins those. TestCloneableLiteralSuccess breaks the lockstep by hard-coding the literal hash the way a real third-party ICloneableV2 must; TestCloneableNearMissSuccess returns the hash of a one-character-different string — a well-formed 32-byte return that reaches the comparison — proving the check is over the exact hash and not over "looks like a hash".

Behaviour matrix

Verdicts: KILLED_PREEXISTING = an existing named test already caught it (coverage audited, nothing added). KILLED_NEW = survived pass 1, killed by a test in this PR. KILLED_STRENGTHENED = already killed, and a test here now kills it deterministically rather than by fuzz luck. EQUIVALENT = argued, never forced.

cloneDeterministic(address,bytes,bytes32) — namespaced entry point

behaviour mutation verdict killing test
msg.sender read INSIDE the library — not a parameter, not tx.origin, not address(this) A1a → tx.origin; A1b → address(this) KILLED_PREEXISTING testCloneDeterministicSenderScoped, testCloneDeterministicMatchesPredict
argument order into effectiveSalt is (msg.sender, salt) A2 swap the two, with casts KILLED_PREEXISTING testCloneDeterministicSenderScoped, testCloneDeterministicManyClonesPerImpl
passes the RAW salt as the 4th argument so NewClone carries it A3 pass the derived salt instead KILLED_PREEXISTING testCloneDeterministicEvent
forwards data unchanged A4 forward "" KILLED_PREEXISTING testCloneDeterministicMatchesPredict, testCloneDeterministicDataNotInDerivation
returns cloneAndInitialize's child A5 discard it, return address(0) KILLED_PREEXISTING testCloneDeterministicMatchesPredict, testCloneDeterministicEvent
(derivation choice) A6 use effectiveOpenSalt KILLED_PREEXISTING testCloneDeterministicSenderScoped, testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt

predictDeterministicAddress(address,bytes32,address) — namespaced prediction

behaviour mutation verdict killing test
factory term is address(this), read internally B1 → msg.sender KILLED_PREEXISTING testCloneDeterministicSaltIsDomainTaggedHash, testCloneDeterministicMatchesPredict (+ new testCloneDeterministicFactoryScoped)
derivation uses the deployer PARAMETER, not msg.sender B2 → msg.sender; G1 default a zero deployer to msg.sender KILLED_PREEXISTING testCloneDeterministicSaltIsDomainTaggedHash, testCloneDeterministicSenderScoped, new testCloneDeterministicPredictCallerIndependent, testCloneDeterministicPredictZeroDeployer
argument order into predictCloneAddress is (factory, implementation, derivedSalt) B3 swap factory and implementation KILLED_PREEXISTING testCloneDeterministicMatchesPredict, testCloneDeterministicSaltIsDomainTaggedHash
mutability: view, no state written H3 drop view from the library function EQUIVALENT (argued below) pinned observationally by new testCloneDeterministicPredictIsStaticCallable
must agree exactly with what cloneDeterministic deploys B5 perturb the derived salt by +1; B6 use the open-salt derivation KILLED_PREEXISTING testCloneDeterministicMatchesPredict

The survey flagged this unit's caller-independence as WEAK — the namespaced predict was never called from two pranked senders. It is now, and testCloneDeterministicPredictZeroDeployer closes the adjacent "helpful default" shape: G1 substitutes msg.sender when deployer == address(0), which the pre-existing suite catches only if the fuzzer happens to draw address(0).

cloneDeterministicOpenSalt(address,bytes,bytes32) — open-salt entry point

behaviour mutation verdict killing test
derivation choice: effectiveOpenSalt(salt, data), not effectiveSalt C1 → effectiveSalt(msg.sender, salt) KILLED_PREEXISTING testCloneDeterministicOpenSaltCallerIndependent
no caller-derived value mixed in (interface MUST NOT) C2a mix in msg.sender; C2b mix in tx.origin KILLED_PREEXISTING testCloneDeterministicOpenSaltCallerIndependent
data is BOTH the derivation input and the initialize payload — same bytes both places C3a derive from "", initialize with data; C3b derive from data, initialize with "" KILLED_PREEXISTING testCloneDeterministicOpenSaltMatchesPredict, testCloneDeterministicOpenSaltDataInDerivation, testCloneDeterministicOpenSaltEvent
passes the RAW salt through for NewClone C4 pass the derived salt instead KILLED_PREEXISTING testCloneDeterministicOpenSaltEvent
returns cloneAndInitialize's child C5 discard it, return address(0) KILLED_PREEXISTING testCloneDeterministicOpenSaltMatchesPredict, testCloneDeterministicOpenSaltDataInDerivation
(argument order) C6 swap (salt, data) KILLED_PREEXISTING testCloneDeterministicOpenSaltCallerIndependent, testCloneDeterministicOpenSaltMatchesPredict

The survey noted "the SAME bytes go to both the derivation and initialize" was only transitively covered. C3a and C3b split that in two and both die, so it is now covered explicitly in both directions.

predictDeterministicAddressOpenSalt(address,bytes,bytes32) — open-salt prediction

behaviour mutation verdict killing test
factory term is address(this) D1 → msg.sender KILLED_PREEXISTING testCloneDeterministicOpenSaltIsDomainTaggedHash, testCloneDeterministicOpenSaltPredictCallerIndependent (+ new testCloneDeterministicOpenSaltFactoryScoped)
derivation: effectiveOpenSalt(salt, data) D2 → namespaced; D7 ignore data; G2 commit to data.length + first word only KILLED_PREEXISTING / KILLED_STRENGTHENED (G2) testCloneDeterministicOpenSaltIsDomainTaggedHash, testCloneDeterministicOpenSaltDataInDerivation, new testCloneDeterministicOpenSaltLargeData
takes no deployer input and reads no caller value D3 mix in msg.sender KILLED_PREEXISTING testCloneDeterministicOpenSaltPredictCallerIndependent
argument order into predictCloneAddress D4 swap factory and implementation KILLED_PREEXISTING testCloneDeterministicOpenSaltIsDomainTaggedHash
mutability: view H4 drop view from the library function EQUIVALENT (argued below) pinned observationally by new testCloneDeterministicOpenSaltPredictIsStaticCallable
must agree exactly with what cloneDeterministicOpenSalt deploys D6 perturb the derived salt by +1 KILLED_PREEXISTING testCloneDeterministicOpenSaltMatchesPredict

The two domain tags

behaviour mutation verdict killing test
keccak256("rain.factory.clone.namespaced") exactly E1 one character KILLED_PREEXISTING testDomainTagsPinned, testEffectiveSaltPreimageShape
keccak256("rain.factory.clone.opensalt") exactly E2 one character KILLED_PREEXISTING testDomainTagsPinned, testEffectiveOpenSaltPreimageShape
the two are distinct (disjointness by construction) E3 collapse both to one string KILLED_PREEXISTING testDomainTagsDistinct, testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat
each occupies word 0, where no caller can place it E4a/E4b move the tag to word 2 KILLED_PREEXISTING testEffectiveSaltPreimageShape, testEffectiveOpenSaltPreimageShape
single source of truth: the library imports rather than re-derives E5a/E5b re-derive from a drifted literal KILLED_PREEXISTING testEffectiveSaltMatchesFormula, testEffectiveOpenSaltMatchesFormula

Strongest unit in the group — every mutation dies to at least two independent tests, and testCloneDeterministicOpenSaltDisjointTagsCloseTheSquat kills E3 by constructing the actual attack rather than by asserting an absence.

ICLONEABLE_V2_SUCCESS — the initialization success sentinel

behaviour mutation verdict killing test
formula: keccak256("ICloneableV2.initialize") exactly F1 one character KILLED_NEW (SURVIVED pass 1) new testCloneableV2SuccessPinned
spec claim: "MUST return the keccak256 hash of the string ICloneableV2.initialize" — a value third parties reproduce F2 a different word entirely KILLED_NEW (SURVIVED pass 1) new testCloneableV2SuccessLiteralIsAcceptedNamespaced, testCloneableV2SuccessLiteralIsAcceptedOpenSalt
the value cloneAndInitialize compares initialize's return against F3 compare against bytes32(0); F4 re-derive from a drifted literal KILLED_PREEXISTING testCloneDeterministicInitializeFailureFails, testCloneDeterministicOpenSaltInitializeFailureFails (+ new testCloneableV2SuccessNearMissIsRejected)

Boundary values

behaviour mutation verdict killing test
the extremes of the salt space are ordinary salts (namespaced) H1 collapse type(uint256).max onto bytes32(0) KILLED_NEW new testCloneDeterministicExtremeSalts
the extremes of the salt space are ordinary salts (open) H2 same, open derivation KILLED_NEW new testCloneDeterministicOpenSaltExtremeSalts

Filed in parallel as #70 by the worker on the derivation functions themselves; these two entry-point tests close the boundary in the direction this group owns.

EQUIVALENT, argued

Both view behaviours. The mutation is not expressible as a scoreable mutant: TestCloneFactory declares its delegating predictDeterministicAddress as view, so dropping view from the library function fails to compile and the probe reports NO-RUN, not SURVIVED:

H3 predictDeterministicAddress is declared non-view:            NO-RUN
H4 predictDeterministicAddressOpenSalt is declared non-view:     NO-RUN
Error (8961): Function cannot be declared as view because this expression (potentially) modifies the state.
  --> test/src/concrete/TestCloneFactory.sol:32:16   (H3)
  --> test/src/concrete/TestCloneFactory.sol:49:16   (H4)

That is the property holding, enforced by the compiler on any delegating concrete — which is the shape the deploy half is specified to use. Recorded as EQUIVALENT rather than forced. Both new …PredictIsStaticCallable tests pin it observationally instead: the prediction answers through a raw STATICCALL at the EVM boundary and returns the same address the typed call does. Nothing else in the suite exercised either prediction where a state write would actually revert.

Adversarial pass — issues filed

Intent oracle taken from the NatSpec on ICloneableV2, ICloneableFactoryV3 and ICloneableFactoryV4, and verified against the source. Each candidate has a repro run against real dependencies. All filed with NEUTRAL triage framing — flagged, not adjudicated.

Invariants exercised that did not yield a candidate, recorded so the absence is on the record: address determinism across orderings; salt derivation at zero and max; initialisation atomicity on the failure path (address left free); caller isolation both ways (namespaced separates, open-salt unifies); factory isolation; implementation-by-address; reentrancy from a hostile initialize (no factory state exists to corrupt, and a same-salt reentrant deploy hits the occupied address and reverts CloneDeploymentFailed); a hostile implementation returning the wrong sentinel (already covered, now also on the exact-hash near miss); colliding salts across deployers and across factories.

Duplicate-filing note

Two of my issues were independently filed by the worker running the neighbouring group within minutes: #64 duplicates #68, and #63 duplicates #66. Same findings, reached separately. Worth closing one of each pair on triage.

QA

  • Discriminating tests: testCloneableV2SuccessPinned, testCloneableV2SuccessLiteralIsAcceptedNamespaced, testCloneableV2SuccessLiteralIsAcceptedOpenSalt, testCloneableV2SuccessNearMissIsRejected, testCloneableV2SuccessNearMissIsWellFormed, testCloneableV2SuccessDistinctFromDomainTags, testCloneDeterministicPredictCallerIndependent, testCloneDeterministicPredictZeroDeployer, testCloneDeterministicPredictIsStaticCallable, testCloneDeterministicFactoryScoped, testCloneDeterministicExtremeSalts, testCloneDeterministicOpenSaltFactoryScoped, testCloneDeterministicOpenSaltImplementationIsByAddress, testCloneDeterministicOpenSaltEmptyData, testCloneDeterministicOpenSaltLargeData, testCloneDeterministicOpenSaltOrderIndependent, testCloneDeterministicOpenSaltExtremeSalts, testCloneDeterministicOpenSaltPredictIsStaticCallable — each verified to fail on the mutated base by mutation-probe, which applies the exact-string mutant, runs the full suite, restores byte-exact, and names the failing tests (verdicts transcribed below). Verified on base as green: forge test 60/60, and 42/42 before any of these were added.
  • Mutations applied: 39 exact-string mutants, plus 3 boundary/mutability probes. Line → mutation → killing test, abbreviated (full 29-behaviour matrix above):
    • src/interface/ICloneableV2.sol:7keccak256("ICloneableV2.initialize")keccak256("ICloneableV2.initialise") (F1) → SURVIVED pass 1, now testCloneableV2SuccessPinned
    • src/interface/ICloneableV2.sol:7keccak256("ICloneableV2.initialize")bytes32(uint256(1)) (F2) → SURVIVED pass 1, now testCloneableV2SuccessLiteralIsAcceptedNamespaced / …OpenSalt
    • src/lib/LibICloneableFactoryV4.sol:166!= ICLONEABLE_V2_SUCCESS!= keccak256("ICloneableV2.initialise") (F4) → testCloneableV2SuccessNearMissIsRejected
    • src/lib/LibICloneableFactoryV4.sol:196effectiveSalt(deployer, salt)effectiveSalt(deployer == address(0) ? msg.sender : deployer, salt) (G1) → testCloneDeterministicPredictZeroDeployer
    • src/lib/LibICloneableFactoryV4.sol:88keccak256(data)keccak256(abi.encodePacked(data.length, firstWord)) (G2) → testCloneDeterministicOpenSaltLargeData
    • src/lib/LibICloneableFactoryV4.sol:75saltsalt == bytes32(type(uint256).max) ? bytes32(0) : salt (H1) → testCloneDeterministicExtremeSalts
    • src/lib/LibICloneableFactoryV4.sol:88 → same max-salt collapse (H2) → testCloneDeterministicOpenSaltExtremeSalts
    • src/lib/LibICloneableFactoryV4.sol:191-194 → drop view (H3) → NO-RUN, compile error on TestCloneFactory.sol:32 — recorded EQUIVALENT, not forced
    • src/lib/LibICloneableFactoryV4.sol:181 → A1a msg.sendertx.origin, A1b →address(this), A2 swapped args, A3 derived salt in NewClone, A4 data"", A5 return address(0), A6 open-salt derivation → all KILLED_PREEXISTING by testCloneDeterministicSenderScoped / …MatchesPredict / …Event / …DataNotInDerivation
    • src/lib/LibICloneableFactoryV4.sol:196 → B1 address(this)msg.sender, B2 deployermsg.sender, B3 swap factory/impl, B5 salt+1, B6 open-salt derivation → KILLED_PREEXISTING by testCloneDeterministicSaltIsDomainTaggedHash / …MatchesPredict / …SenderScoped
    • src/lib/LibICloneableFactoryV4.sol:211 → C1 namespaced derivation, C2a/C2b mix in msg.sender/tx.origin, C3a derive from "", C3b initialize with "", C4 derived salt in NewClone, C5 return address(0), C6 swapped args → KILLED_PREEXISTING by testCloneDeterministicOpenSaltCallerIndependent / …DataInDerivation / …MatchesPredict / …Event
    • src/lib/LibICloneableFactoryV4.sol:228 → D1 factory→msg.sender, D2 namespaced derivation, D3 mix in msg.sender, D4 swap factory/impl, D6 salt+1, D7 ignore data → KILLED_PREEXISTING by testCloneDeterministicOpenSaltIsDomainTaggedHash / …PredictCallerIndependent / …DataInDerivation
    • src/interface/ICloneableFactoryV4.sol:15,24 → E1/E2 one-character tag drift, E3 collapse both tags to one string → KILLED_PREEXISTING by testDomainTagsPinned / testDomainTagsDistinct / testEffective*PreimageShape / …DisjointTagsCloseTheSquat
    • src/lib/LibICloneableFactoryV4.sol:75,88 → E4a/E4b tag moved from word 0 to word 2, E5a/E5b tag re-derived from a drifted literal → KILLED_PREEXISTING by testEffective*PreimageShape / testEffective*MatchesFormula
  • Oracle: the NatSpec, not the implementation. ICloneableV2.sol:38-42 fixes the sentinel to keccak256("ICloneableV2.initialize"); ICloneableFactoryV4.sol:38-48 fixes both effective-salt formulas to exact bytes; ICloneableFactoryV3.sol:33-50 fixes the msg.sender namespacing, the atomic initialize-and-verify, and NewClone. Every expected value is recomputed from the literal string or the written-out formula — never read back from the constant or the library. TestCloneableLiteralSuccess hard-codes keccak256("ICloneableV2.initialize") rather than importing ICLONEABLE_V2_SUCCESS, which is what breaks the lockstep that let F1/F2 survive. OZ Clones.predictDeterministicAddress remains the foreign EIP-1167 oracle for the address construction.
  • Category check: group asks for 6 units / 29 behaviours — namespaced clone (5), namespaced predict (5), open-salt clone (5), open-salt predict (6), the two domain tags (5), the success sentinel (3). Covered: 27 KILLED by a named test, 2 EQUIVALENT with the compile-error evidence transcribed (view on both predictions). 0 SURVIVED, 0 UNPROBED. Adversarial half: 4 issues filed with both audit and adversarial labels — InitializationFailed is not raised when the implementation does not implement ICloneableV2.initialize — caller gets bare 0x #61, predictDeterministicAddressOpenSalt NatSpec claims non-zero code implies initialized state, but the clone has code before initialize runs #62, ICloneableFactoryV3 cross-network determinism claim omits the implementation address, contradicting ICloneableFactoryV4 #63, testCheckImplementationCodeEtched fails on unmutated main: vm.etch rejects fuzzed 0xEF-leading code #64 — and the falsification attempts that produced no candidate are listed under "Adversarial pass" above rather than omitted.

Probe evidence, transcribed

Harness taken from .github/workflows/rainix-sol.yaml → rainix sol-shell; suite command is forge test, run through nix develop .#sol-shell.

Baseline, before any test of mine was written. Green, and green repeatably only after the #64 fix:

$ forge test          # x6, no seed pinned
Ran 6 test suites: 42 tests passed, 0 failed, 0 skipped   (x6)

Before the fix, testCheckImplementationCodeEtched failed 8/8 standalone with vm.etch: failed to create bytecode: Eip7702 is not 23 bytes long, and intermittently in whole-suite runs — where it showed up as a spurious killer of mutants the suite does not actually detect, including both ICLONEABLE_V2_SUCCESS mutants. That is why it had to be fixed before anything could be attributed.

Pass 1 — entire behaviour list against the PRE-EXISTING suite, none of my tests written:

baseline: green (42 passed)
... 36 mutants ...
== 34/36 killed; survived: 2; no-run: 0; harness errors: 0

F1 ICLONEABLE_V2_SUCCESS string changed by one character: SURVIVED
F2 ICLONEABLE_V2_SUCCESS is a different word entirely:    SURVIVED

Every one of the 34 kills credits a named pre-existing test (full attribution in the matrix above). The two survivors were the worklist.

Pass 2 — with this PR's tests, plus mutants aimed squarely at them:

baseline: green (60 passed)
== 39/39 killed; survived: 0; no-run: 0; harness errors: 0

F1 ...: KILLED — killed by: testCloneableV2SuccessLiteralIsAcceptedNamespaced,
        testCloneableV2SuccessLiteralIsAcceptedOpenSalt,
        testCloneableV2SuccessNearMissIsRejected,
        testCloneableV2SuccessNearMissIsWellFormed, testCloneableV2SuccessPinned
F2 ...: KILLED — killed by: testCloneableV2SuccessLiteralIsAcceptedNamespaced,
        testCloneableV2SuccessLiteralIsAcceptedOpenSalt, testCloneableV2SuccessPinned
G1 zero-deployer default:  KILLED — testCloneDeterministicPredictZeroDeployer,
        testCloneDeterministicPredictCallerIndependent, ...
G2 data-prefix commitment: KILLED — testCloneDeterministicOpenSaltLargeData, ...
F4 sentinel re-derived and drifted: KILLED — testCloneableV2SuccessNearMissIsRejected, ...

Boundary and mutability probes:

H1 namespaced max salt collapsed onto zero: KILLED — testCloneDeterministicExtremeSalts
H2 open-salt max salt collapsed onto zero:  KILLED — testCloneDeterministicOpenSaltExtremeSalts
H3 predictDeterministicAddress non-view:            NO-RUN (compile error — see EQUIVALENT above)
H4 predictDeterministicAddressOpenSalt non-view:    NO-RUN (compile error — same, TestCloneFactory.sol:49)

Suite: 42 → 60 tests, all passing, forge fmt clean. Mutation targets were never placed in test code; the mutants file was never committed. CI (rainix-sol) green on the pushed branch.

Confirmation sweep — 9 further mutants on the same four entry points and two derivations, shapes not in the behaviour list, to check nothing survives that the list did not anticipate:

baseline: green (60 passed)
I1 cloneDeterministic skips the derivation, CREATE2s the raw caller salt:        KILLED
I2 cloneDeterministicOpenSalt skips the derivation:                              KILLED
I3 predictDeterministicAddress predicts on the raw caller salt:                   KILLED
I4 predictDeterministicAddressOpenSalt predicts on the raw caller salt:           KILLED
I5 predictDeterministicAddress always answers address(0):                         KILLED
I6 predictDeterministicAddressOpenSalt always answers address(0):                 KILLED
I7 namespaced preimage packs the deployer to 20 bytes, not a full word:           KILLED
I8 open-salt derivation hashes data inline instead of by hash:                    KILLED
I9 cloneDeterministic emits keccak256(data) in NewClone's salt slot:              KILLED
== 9/9 killed; survived: 0; no-run: 0; harness errors: 0

I7 is the one worth naming: abi.encodeabi.encodePacked shortens the namespaced preimage from 96 bytes to 84 by packing deployer to 20 bytes, and dies to testEffectiveSaltPreimageShape's explicit assertEq(preimage.length, 96). That length assertion is doing real work.

Running total across all passes: 52 mutants, 0 SURVIVED, 2 NO-RUN (H3 and H4, the two EQUIVALENT view probes).

Unprobed

Recorded rather than declared done:

  • view on both predictions is EQUIVALENT-by-compiler, not mutation-probed. Argued above with the NO-RUN evidence; the STATICCALL tests are the observational substitute, not a mutation kill.
  • A caller-dependence in the namespaced prediction that agrees for the default test caller — a mutant correct when called from the test contract and wrong from any other — is not among the mutants here. Every natural formulation I could write (msg.sender substituted for the factory, for the deployer, or mixed into the salt) is caught by testCloneDeterministicSaltIsDomainTaggedHash's fixed-caller/fuzzed-deployer shape, so the residue is contrived. testCloneDeterministicPredictCallerIndependent now asserts the property directly regardless.
  • Preimage-collision resistance of either derivation is assumed, not probed. testDerivationsDisjoint and testCloneDeterministicOpenSaltDiffersFromSenderNamespaced can only fail on a keccak256 collision; the reachable disjointness is what …DisjointTagsCloseTheSquat covers.
  • data larger than 10KB on the open-salt path is untested; 10KB was chosen to stay inside a reasonable gas budget for a 2048-run fuzz.

Do not merge without review — this is an audit deliverable, and #61/#62/#63 are open questions for the authors rather than decisions I made.

🤖 Generated with Claude Code

… coverage

Baseline repair. testCheckImplementationCodeEtched fails on unmutated main,
8/8 runs standalone: the fuzzer reaches `code` beginning 0xef01 and vm.etch
rejects it as a malformed EIP-7702 delegation designator. It intermittently
poisoned whole-suite runs too, showing up as a spurious killer of mutants the
suite does not detect. EIP-3541 makes 0xEF-leading runtime code unreachable on
chain anyway, so excluding it narrows the fuzz domain to the domain the guard
is specified over. Filed as #64.

ICLONEABLE_V2_SUCCESS had zero direct coverage — the highest-value gap in the
repo. TestCloneable imports the same constant the library compares against, so
both sides of that comparison moved in lockstep and mutating the literal string
left all 42 tests green. New test/src/interface/ICloneableV2.t.sol pins the
sentinel to its literal string the way testDomainTagsPinned pins the domain
tags, and breaks the lockstep end to end with TestCloneableLiteralSuccess,
which hard-codes keccak256("ICloneableV2.initialize") the way a real
third-party ICloneableV2 must. TestCloneableNearMissSuccess returns the hash of
a one-character-different string — a well-formed 32-byte return that reaches
the comparison — proving the check is over the exact hash.

Adversarial-pass additions on the four clone entry points: caller-independence
and a zero deployer for the namespaced prediction (the one derivation input
never asserted from two pranked senders), factory scoping for both derivations,
implementation-by-address, empty and 10KB data, order independence, extreme
salts, and STATICCALL-ability of both predictions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 21 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 00512745-9e5c-40d3-8b94-44ed6e47e324

📥 Commits

Reviewing files that changed from the base of the PR and between c1c2afd and 09728a8.

📒 Files selected for processing (9)
  • .gas-snapshot
  • test/concrete/TestCloneFactory.sol
  • test/concrete/TestCloneable.sol
  • test/concrete/TestCloneableFailure.sol
  • test/src/concrete/TestCloneable.sol
  • test/src/interface/ICloneableV2.t.sol
  • test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol
  • test/src/lib/LibICloneableFactoryV4.cloneDeterministic.t.sol
  • test/src/lib/LibICloneableFactoryV4.cloneDeterministicOpenSalt.t.sol

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Queuing a review — the first attempt hit the OSS rate limit before reading the diff.

Orientation for the review, since this is an audit deliverable rather than a feature:

  • Nothing under src/ is touched. The diff is tests plus two ICloneableV2 fixtures.
  • test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol adds vm.assume(code[0] != 0xef). That is a baseline repair, not a convenience: the test fails 8/8 standalone on unmutated main because vm.etch rejects 0xef01-prefixed code as a malformed EIP-7702 delegation designator. EIP-3541 makes 0xEF-leading runtime code unreachable on chain, so the assume narrows the fuzz domain to the domain the guard is actually specified over. Filed separately as testCheckImplementationCodeEtched fails on unmutated main: vm.etch rejects fuzzed 0xEF-leading code #64 — please push back if you think a pinned [fuzz] seed would be the better fix.
  • TestCloneableLiteralSuccess deliberately hard-codes keccak256("ICloneableV2.initialize") instead of importing ICLONEABLE_V2_SUCCESS. That duplication is the point: TestCloneable imports the same constant the library compares against, so both sides moved in lockstep and mutating the sentinel left all 42 tests green. Please do not suggest replacing the literal with the import.
  • testCloneDeterministicOpenSaltLargeData builds 10KB of data in a loop under a 2048-run fuzz. Mean gas 6.3M, suite still under a second — but flag it if you think that is too heavy for CI.

Three issues in this PR's description (#61, #62, #63) are open questions for the authors, deliberately framed neutrally rather than adjudicated. Correctness review of the reasoning in those is welcome.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Your plan includes PR reviews subject to rate limits. More reviews will be available in 47 minutes.

baku-ccron and others added 2 commits August 24, 2026 12:17
`test/src/**` mirrors `src/**` and holds the `.t.sol` suites. This repo is
the library half of the split — there is no `src/concrete/`, the concrete
lives in rain.factory.deploy — so `test/src/concrete/` mirrored nothing.
Test SUPPORT code (harnesses, mocks, fixtures) belongs outside the mirror,
in `test/concrete/`, `test/lib/`, `test/abstract/`, as in rain.deploy and
rain.math.float.

Pure move plus the import paths that follow it. No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntable code

Three parallel AMT branches (#76, #77, #78) each grew their own variant of
`TestCloneable` because it satisfies neither of `ICloneableV2`'s normative
MUSTs and moves in lockstep with the constant the library compares against.
Fix it once, here, so the branches converge on one fixture instead of four:

- `initialize` can NOT be called more than once. That is the interface's
  first MUST and no fixture honoured it.
- The RECOMMENDED typed overload is present and reverts
  `InitializeSignatureFn` always, as the interface requires.
- The success sentinel is written out from the LITERAL string the interface
  names, not imported from `ICLONEABLE_V2_SUCCESS`. Importing it put both
  sides of the library's comparison in lockstep: the constant could drift and
  every flow test would still pass, because the fixture drifted with it. A
  third party hard-codes `keccak256("ICloneableV2.initialize")`, so the
  fixture does too, and every existing flow test now discriminates a drift.

Separately, `testCheckImplementationCodeEtched` could fail for a harness
reason: `vm.etch` parses a `0xef01` prefix as an EIP-7702 delegation
designator and rejects anything that is not exactly the 23-byte designator.
EIP-3541 forbids deploying any `0xef`-leading code at all, so such code cannot
exist at an implementation address on any chain and the guard is not specified
over it; the fuzz domain is narrowed to code that could actually exist. The
guard only ever reads code LENGTH, so nothing about the property changes.

Gas snapshot regenerated for the extra `SSTORE` the initialization guard costs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baku-ccron and others added 4 commits August 24, 2026 12:27
# Conflicts:
#	test/concrete/TestCloneableLiteralSuccess.sol
#	test/concrete/TestCloneableNearMissSuccess.sol
#	test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol
Three parallel AMT branches each invented their own `ICloneableV2` fixture on
top of the two that already existed. The merge from
`2026-08-24-test-fixtures-out-of-src-mirror` resolves this branch onto the
shared set.

`TestCloneableLiteralSuccess` and `TestCloneableNearMissSuccess` are both
deleted, because the base branch made them redundant rather than because the
coverage they carried was expendable:

- `TestCloneableLiteralSuccess` existed only to hard-code the success sentinel
  as a literal, so that at least one fixture was not in lockstep with the
  constant the library compares against. `TestCloneable` now does exactly that
  itself, for every flow test in the suite rather than for these two, so the
  separate fixture has nothing left to add. `testCloneableV2SuccessLiteral*`
  keep their names and their assertions; only the implementation they clone
  changes.
- `TestCloneableNearMissSuccess` returned one fixed near-miss hash.
  `TestCloneableFailure` returns whatever `bytes32` its data decodes to, so
  handing it `abi.encode(keccak256("ICloneableV2.initialise"))` produces the
  same near miss and the test now names the near-miss value at the call site
  instead of burying it in a fixture. The `data` fuzz parameter goes, because
  the whole point of the case is one exact non-success 32-byte return.

Mutation-checked, not assumed: with the library's
`initialize(data) != ICLONEABLE_V2_SUCCESS` mutated to `== bytes32(0)`,
`testCloneableV2SuccessNearMissIsRejected` still fails. The near-miss case
survives the consolidation with its discriminating power intact.

Gas snapshot regenerated: it moves for the extra `SSTORE` the base branch's
initialization guard costs on every `TestCloneable` clone, as well as for this
branch's new tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per CodeRabbit on this PR. The finding is correct and the comment on
`vm.assume(code[0] != 0xef)` was overclaiming.

The comment said no implementation on any chain can have `0xef`-leading code.
EIP-3541 does forbid DEPLOYING it, so no CREATE or CREATE2 can produce it — but
EIP-7702 leaves exactly one way an account can hold it anyway: a delegation
designator, `0xef0100 || address`, exactly 23 bytes. `EXTCODESIZE` on a
delegated EOA returns 23, not zero. The assume was silently excluding a case
that is real, behind a comment saying it was not.

`testCheckImplementationCodeEip7702Designator` pins it as a fixed case, since
the fuzz test cannot reach it. It PASSES the guard, which is the part worth
having on the record: a code-SIZE check cannot tell an implementation contract
from an EOA that has delegated, and a delegation is REVOCABLE by the account
holder where deployed code is not. Callers wanting an immutable implementation
do not get that from this guard.

Mutation-checked, not assumed: with the guard mutated to
`code.length == 0 || code[0] == 0xef`, this is the ONLY test in the suite that
fails (42 pass, 1 fail). The fuzz test cannot kill that mutant by construction,
because its assume excludes the input that would.

Scoped honestly in the NatSpec: `foundry.toml` pins `evm_version = "cancun"`,
which predates EIP-7702, so the test asserts that the 23-byte designator is
storable and passes the SIZE check. It does not exercise, and does not claim,
the execution semantics of delegation.

The rest of the assume stands: `0xef`-leading blobs of any OTHER length cannot
exist on any chain, and `vm.etch` rejects them outright ("Eip7702 is not 23
bytes long"), which is what was breaking the fuzz test 8 runs in 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Re-queuing — the earlier request was rate limited, and the branch has changed since.

Orientation: nothing under src/ is touched. TestCloneableLiteralSuccess and TestCloneableNearMissSuccess are deleted because the base branch made them redundant, not because their coverage was expendable. TestCloneable now hard-codes the literal success hash itself, so the separate literal fixture has nothing left to add. The near miss is now built at the call site — TestCloneableFailure returns whatever bytes32 its data decodes to, so abi.encode(keccak256("ICloneableV2.initialise")) produces the same one-character-off hash the deleted fixture hard-coded.

Mutation-checked rather than assumed: with the library's initialize(data) != ICLONEABLE_V2_SUCCESS mutated to == bytes32(0), testCloneableV2SuccessNearMissIsRejected still fails, so the consolidation kept the discriminating power.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

@thedavidmeister I will review the current branch. I will verify the consolidated success and near-miss coverage.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant