AMT coverage: g3-libicloneablefactoryv4-clone - #77
Conversation
… 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>
|
Warning Review limit reachedNext included review available in 21 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
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 |
|
@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:
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. |
|
Your plan includes PR reviews subject to rate limits. More reviews will be available in 47 minutes. |
`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>
# 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>
# Conflicts: # .gas-snapshot
|
@coderabbitai review Re-queuing — the earlier request was rate limited, and the branch has changed since. Orientation: nothing under Mutation-checked rather than assumed: with the library's |
|
|
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:
TestCloneableLiteralSuccessTestCloneable, which now hard-codes the literalkeccak256("ICloneableV2.initialize")itself rather than importingICLONEABLE_V2_SUCCESS. The lockstep-breaking property moved to THE conforming fixture, so every flow test in the suite has it instead of two.TestCloneableNearMissSuccessTestCloneableFailureplusabi.encode(keccak256("ICloneableV2.initialise"))at the call site. It returns whateverbytes32its 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_SUCCESSmutated to== bytes32(0),testCloneableV2SuccessNearMissIsRejectedstill fails.Paths under
test/src/concrete/below are nowtest/concrete/—test/src/**mirrorssrc/**, and fixtures have nosrccounterpart.Adversarial mutation-testing pass over group g3-libicloneablefactoryv4-clone — the four
LibICloneableFactoryV4clone entry points, the twoICloneableFactoryV4domain tags, and theICLONEABLE_V2_SUCCESSsentinel. 29 behaviours.Scanned commit
c1c2afd.What changed
testCheckImplementationCodeEtchedfails on unmutatedmain— 8/8 runs standalone. Filed as testCheckImplementationCodeEtched fails on unmutated main: vm.etch rejects fuzzed 0xEF-leading code #64. Nothing could be probed honestly until this was fixed.ICLONEABLE_V2_SUCCESScoverage, the one real gap. Newtest/src/interface/ICloneableV2.t.solplus two fixtures.STATICCALL-ability of both predictions.audit+adversarial: 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.No file under
src/is touched.The gap that mattered
ICLONEABLE_V2_SUCCESShad zero direct coverage.TestCloneableimports 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:This is a value third parties reproduce —
ICloneableV2tells every implementer to returnkeccak256("ICloneableV2.initialize")— so it is consensus-critical in the same way the two domain tags are, and it is now pinned the same waytestDomainTagsPinnedpins those.TestCloneableLiteralSuccessbreaks the lockstep by hard-coding the literal hash the way a real third-partyICloneableV2must;TestCloneableNearMissSuccessreturns 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 pointmsg.senderread INSIDE the library — not a parameter, nottx.origin, notaddress(this)tx.origin; A1b →address(this)testCloneDeterministicSenderScoped,testCloneDeterministicMatchesPredicteffectiveSaltis(msg.sender, salt)testCloneDeterministicSenderScoped,testCloneDeterministicManyClonesPerImplNewClonecarries ittestCloneDeterministicEventdataunchanged""testCloneDeterministicMatchesPredict,testCloneDeterministicDataNotInDerivationcloneAndInitialize's childaddress(0)testCloneDeterministicMatchesPredict,testCloneDeterministicEventeffectiveOpenSalttestCloneDeterministicSenderScoped,testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSaltpredictDeterministicAddress(address,bytes32,address)— namespaced predictionaddress(this), read internallymsg.sendertestCloneDeterministicSaltIsDomainTaggedHash,testCloneDeterministicMatchesPredict(+ newtestCloneDeterministicFactoryScoped)deployerPARAMETER, notmsg.sendermsg.sender; G1 default a zero deployer tomsg.sendertestCloneDeterministicSaltIsDomainTaggedHash,testCloneDeterministicSenderScoped, newtestCloneDeterministicPredictCallerIndependent,testCloneDeterministicPredictZeroDeployerpredictCloneAddressis(factory, implementation, derivedSalt)testCloneDeterministicMatchesPredict,testCloneDeterministicSaltIsDomainTaggedHashview, no state writtenviewfrom the library functiontestCloneDeterministicPredictIsStaticCallablecloneDeterministicdeploystestCloneDeterministicMatchesPredictThe survey flagged this unit's caller-independence as WEAK — the namespaced predict was never called from two pranked senders. It is now, and
testCloneDeterministicPredictZeroDeployercloses the adjacent "helpful default" shape: G1 substitutesmsg.senderwhendeployer == address(0), which the pre-existing suite catches only if the fuzzer happens to drawaddress(0).cloneDeterministicOpenSalt(address,bytes,bytes32)— open-salt entry pointeffectiveOpenSalt(salt, data), noteffectiveSalteffectiveSalt(msg.sender, salt)testCloneDeterministicOpenSaltCallerIndependentmsg.sender; C2b mix intx.origintestCloneDeterministicOpenSaltCallerIndependentdatais BOTH the derivation input and the initialize payload — same bytes both places"", initialize withdata; C3b derive fromdata, initialize with""testCloneDeterministicOpenSaltMatchesPredict,testCloneDeterministicOpenSaltDataInDerivation,testCloneDeterministicOpenSaltEventNewClonetestCloneDeterministicOpenSaltEventcloneAndInitialize's childaddress(0)testCloneDeterministicOpenSaltMatchesPredict,testCloneDeterministicOpenSaltDataInDerivation(salt, data)testCloneDeterministicOpenSaltCallerIndependent,testCloneDeterministicOpenSaltMatchesPredictThe 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 predictionaddress(this)msg.sendertestCloneDeterministicOpenSaltIsDomainTaggedHash,testCloneDeterministicOpenSaltPredictCallerIndependent(+ newtestCloneDeterministicOpenSaltFactoryScoped)effectiveOpenSalt(salt, data)data; G2 commit todata.length+ first word onlytestCloneDeterministicOpenSaltIsDomainTaggedHash,testCloneDeterministicOpenSaltDataInDerivation, newtestCloneDeterministicOpenSaltLargeDatamsg.sendertestCloneDeterministicOpenSaltPredictCallerIndependentpredictCloneAddresstestCloneDeterministicOpenSaltIsDomainTaggedHashviewviewfrom the library functiontestCloneDeterministicOpenSaltPredictIsStaticCallablecloneDeterministicOpenSaltdeploystestCloneDeterministicOpenSaltMatchesPredictThe two domain tags
keccak256("rain.factory.clone.namespaced")exactlytestDomainTagsPinned,testEffectiveSaltPreimageShapekeccak256("rain.factory.clone.opensalt")exactlytestDomainTagsPinned,testEffectiveOpenSaltPreimageShapetestDomainTagsDistinct,testCloneDeterministicOpenSaltDisjointTagsCloseTheSquattestEffectiveSaltPreimageShape,testEffectiveOpenSaltPreimageShapetestEffectiveSaltMatchesFormula,testEffectiveOpenSaltMatchesFormulaStrongest unit in the group — every mutation dies to at least two independent tests, and
testCloneDeterministicOpenSaltDisjointTagsCloseTheSquatkills E3 by constructing the actual attack rather than by asserting an absence.ICLONEABLE_V2_SUCCESS— the initialization success sentinelkeccak256("ICloneableV2.initialize")exactlytestCloneableV2SuccessPinnedICloneableV2.initialize" — a value third parties reproducetestCloneableV2SuccessLiteralIsAcceptedNamespaced,testCloneableV2SuccessLiteralIsAcceptedOpenSaltcloneAndInitializecomparesinitialize's return againstbytes32(0); F4 re-derive from a drifted literaltestCloneDeterministicInitializeFailureFails,testCloneDeterministicOpenSaltInitializeFailureFails(+ newtestCloneableV2SuccessNearMissIsRejected)Boundary values
type(uint256).maxontobytes32(0)testCloneDeterministicExtremeSaltstestCloneDeterministicOpenSaltExtremeSaltsFiled 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
viewbehaviours. The mutation is not expressible as a scoreable mutant:TestCloneFactorydeclares its delegatingpredictDeterministicAddressasview, so droppingviewfrom the library function fails to compile and the probe reports NO-RUN, not SURVIVED: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
…PredictIsStaticCallabletests pin it observationally instead: the prediction answers through a rawSTATICCALLat 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,ICloneableFactoryV3andICloneableFactoryV4, and verified against the source. Each candidate has a repro run against real dependencies. All filed with NEUTRAL triage framing — flagged, not adjudicated.InitializationFailedis not raised when the implementation does not implementICloneableV2.initialize. The sentinel's documented purpose is to catch "the implementation doesn't supportICloneableV2". For the ordinary shapes of exactly that (no such selector, a silent fallback, a short return) the comparison is never reached and the caller gets bare0x, not the typed error. Verified on three implementations, both entry points.predictDeterministicAddressOpenSaltNatSpec claims non-zero code implies initialized state.cloneAndInitializedeploys, emitsNewClone, then initializes, so a third party reached duringinitializesees the pinned address with 45 bytes of code and empty state. Verified:code len during init: 45,sData during init: 0x,sData after init: 0xdeadbeef.ICloneableFactoryV3's cross-network determinism claim omits the implementation address, whichICloneableFactoryV4explicitly states is not sufficient. V3 is published standalone for pinned consumers who may never read V4. Documentation-correctness only; no code change implied.testCheckImplementationCodeEtchedfails on unmutatedmain. Fixed here.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 revertsCloneDeploymentFailed); 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
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 bymutation-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 test60/60, and 42/42 before any of these were added.src/interface/ICloneableV2.sol:7→keccak256("ICloneableV2.initialize")→keccak256("ICloneableV2.initialise")(F1) → SURVIVED pass 1, nowtestCloneableV2SuccessPinnedsrc/interface/ICloneableV2.sol:7→keccak256("ICloneableV2.initialize")→bytes32(uint256(1))(F2) → SURVIVED pass 1, nowtestCloneableV2SuccessLiteralIsAcceptedNamespaced/…OpenSaltsrc/lib/LibICloneableFactoryV4.sol:166→!= ICLONEABLE_V2_SUCCESS→!= keccak256("ICloneableV2.initialise")(F4) →testCloneableV2SuccessNearMissIsRejectedsrc/lib/LibICloneableFactoryV4.sol:196→effectiveSalt(deployer, salt)→effectiveSalt(deployer == address(0) ? msg.sender : deployer, salt)(G1) →testCloneDeterministicPredictZeroDeployersrc/lib/LibICloneableFactoryV4.sol:88→keccak256(data)→keccak256(abi.encodePacked(data.length, firstWord))(G2) →testCloneDeterministicOpenSaltLargeDatasrc/lib/LibICloneableFactoryV4.sol:75→salt→salt == bytes32(type(uint256).max) ? bytes32(0) : salt(H1) →testCloneDeterministicExtremeSaltssrc/lib/LibICloneableFactoryV4.sol:88→ same max-salt collapse (H2) →testCloneDeterministicOpenSaltExtremeSaltssrc/lib/LibICloneableFactoryV4.sol:191-194→ dropview(H3) → NO-RUN, compile error onTestCloneFactory.sol:32— recorded EQUIVALENT, not forcedsrc/lib/LibICloneableFactoryV4.sol:181→ A1amsg.sender→tx.origin, A1b →address(this), A2 swapped args, A3 derived salt inNewClone, A4data→"", A5 returnaddress(0), A6 open-salt derivation → all KILLED_PREEXISTING bytestCloneDeterministicSenderScoped/…MatchesPredict/…Event/…DataNotInDerivationsrc/lib/LibICloneableFactoryV4.sol:196→ B1address(this)→msg.sender, B2deployer→msg.sender, B3 swap factory/impl, B5 salt+1, B6 open-salt derivation → KILLED_PREEXISTING bytestCloneDeterministicSaltIsDomainTaggedHash/…MatchesPredict/…SenderScopedsrc/lib/LibICloneableFactoryV4.sol:211→ C1 namespaced derivation, C2a/C2b mix inmsg.sender/tx.origin, C3a derive from"", C3b initialize with"", C4 derived salt inNewClone, C5 returnaddress(0), C6 swapped args → KILLED_PREEXISTING bytestCloneDeterministicOpenSaltCallerIndependent/…DataInDerivation/…MatchesPredict/…Eventsrc/lib/LibICloneableFactoryV4.sol:228→ D1 factory→msg.sender, D2 namespaced derivation, D3 mix inmsg.sender, D4 swap factory/impl, D6 salt+1, D7 ignoredata→ KILLED_PREEXISTING bytestCloneDeterministicOpenSaltIsDomainTaggedHash/…PredictCallerIndependent/…DataInDerivationsrc/interface/ICloneableFactoryV4.sol:15,24→ E1/E2 one-character tag drift, E3 collapse both tags to one string → KILLED_PREEXISTING bytestDomainTagsPinned/testDomainTagsDistinct/testEffective*PreimageShape/…DisjointTagsCloseTheSquatsrc/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 bytestEffective*PreimageShape/testEffective*MatchesFormulaICloneableV2.sol:38-42fixes the sentinel tokeccak256("ICloneableV2.initialize");ICloneableFactoryV4.sol:38-48fixes both effective-salt formulas to exact bytes;ICloneableFactoryV3.sol:33-50fixes themsg.sendernamespacing, the atomic initialize-and-verify, andNewClone. Every expected value is recomputed from the literal string or the written-out formula — never read back from the constant or the library.TestCloneableLiteralSuccesshard-codeskeccak256("ICloneableV2.initialize")rather than importingICLONEABLE_V2_SUCCESS, which is what breaks the lockstep that let F1/F2 survive. OZClones.predictDeterministicAddressremains the foreign EIP-1167 oracle for the address construction.viewon both predictions). 0 SURVIVED, 0 UNPROBED. Adversarial half: 4 issues filed with bothauditandadversariallabels — 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→ rainixsol-shell; suite command isforge test, run throughnix develop .#sol-shell.Baseline, before any test of mine was written. Green, and green repeatably only after the #64 fix:
Before the fix,
testCheckImplementationCodeEtchedfailed 8/8 standalone withvm.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 bothICLONEABLE_V2_SUCCESSmutants. 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:
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:
Boundary and mutability probes:
Suite: 42 → 60 tests, all passing,
forge fmtclean. 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:
I7 is the one worth naming:
abi.encode→abi.encodePackedshortens the namespaced preimage from 96 bytes to 84 by packingdeployerto 20 bytes, and dies totestEffectiveSaltPreimageShape's explicitassertEq(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
viewprobes).Unprobed
Recorded rather than declared done:
viewon both predictions is EQUIVALENT-by-compiler, not mutation-probed. Argued above with the NO-RUN evidence; theSTATICCALLtests are the observational substitute, not a mutation kill.msg.sendersubstituted for the factory, for the deployer, or mixed into the salt) is caught bytestCloneDeterministicSaltIsDomainTaggedHash's fixed-caller/fuzzed-deployer shape, so the residue is contrived.testCloneDeterministicPredictCallerIndependentnow asserts the property directly regardless.testDerivationsDisjointandtestCloneDeterministicOpenSaltDiffersFromSenderNamespacedcan only fail on a keccak256 collision; the reachable disjointness is what…DisjointTagsCloseTheSquatcovers.datalarger 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