Skip to content

AMT coverage: g2-libicloneablefactoryv4-predi - #78

Open
thedavidmeister wants to merge 8 commits into
mainfrom
2026-08-24-amt-g2-libicloneablefactoryv4-predi
Open

AMT coverage: g2-libicloneablefactoryv4-predi#78
thedavidmeister wants to merge 8 commits into
mainfrom
2026-08-24-amt-g2-libicloneablefactoryv4-predi

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Adversarial mutation-test coverage for group g2-libicloneablefactoryv4-predi
(29 behaviours across predictCloneAddress, checkImplementationCode, the
shared cloneAndInitialize tail and cloneDeterministic's derivation choice),
scanned at c1c2afd3d88405d3228cb3b21e55c9d63ba8f5be.

38 exact-string mutants were probed with mutation-probe against the
pre-existing suite first (attribution pass), then again with the new tests.
Final score: 37/38 KILLED, 0 SURVIVED — 32 by pre-existing tests, 5 by the
tests added here.
The 38th is uncompilable rather than survivable and is
recorded as EQUIVALENT below.

Adversarial findings filed during this run (audit + adversarial): #72, #73,
#74. #72 turned out to duplicate #61, filed independently by a parallel pass over
the same unit — I could not comment on or close it from this session, so it is
noted here instead; consolidate on #61.

The first commit also fixes the pre-existing test defect reported in #64 and #68.

The baseline was red before anything else could happen

testCheckImplementationCodeEtched fuzzes arbitrary code bytes straight into
vm.etch. Any draw whose first byte is 0xef is rejected by the cheatcode
itself — 0xef00 is the EOF magic, 0xef01 the EIP-7702 delegation magic, and
an 0xef01… blob that is not exactly the 23-byte designator fails with
Eip7702 is not 23 bytes long. It is seed-dependent, so it looked green often
enough to be missed.

This is not cosmetic: the first probe pass ran on a seed that drew such a blob,
and three mutants — C16 (NewClone after initialize), C17 (a stray call on the
proxy before initialize) and C23 (a reverting initialize swallowed) — were
scored KILLED by a test failing for a reason that had nothing to do with them.
With the flake fixed, all three came back SURVIVED, which is what they really
were. The attribution numbers in this PR are from the repaired baseline.

Behaviour matrix

Verdict legend: pre = killed by a pre-existing test in the attribution pass,
before any test was written here; new = killed by a test added in this PR;
equiv = the mutant cannot exist as compilable code.

predictCloneAddress(address,address,bytes32)

Behaviour Mutation Verdict Killing test
calls cloneCreationCode(implementation) rather than taking a codehash A01 hash abi.encodePacked(implementation) instead of the creation code pre testPredictCloneAddressMatchesOZ
truncation uint256 -> uint160 -> address takes the LOW 20 bytes A02 >> 96 first, taking the HIGH 20 pre testPredictCloneAddressIsCreate2Formula
encoding is abi.encodePacked, not abi.encode A03 abi.encode pre testPredictCloneAddressMatchesOZ
purity: factory is a parameter, not address(this) A04 factory pinned to address(0); A05 factory/derivedSalt swapped pre testPredictCloneAddressMatchesOZ

checkImplementationCode(address)

Behaviour Mutation Verdict Killing test
side-effect-free read of implementation.code.length (EXTCODESIZE) B01 reads address(this).code.length pre testCheckImplementationCodeZero
comparison == 0 (boundary: exactly 0 reverts, exactly 1 byte passes) B02 != 0; B03 guard never fires; B04 < 2 pre B02 testCheckImplementationCodeContract; B03 testCheckImplementationCodeZero; B04 testCheckImplementationCodeEtched
revert path: ZeroImplementationCodeSize, typed, no arguments B06 reverts CloneDeploymentFailed instead pre testCheckImplementationCodeZero
pass-through: the guard validates SIZE, not content B05 also rejects code whose first byte is 0x00 pre testCheckImplementationCodeEtched
mutability: view B07 drop view equiv see EQUIVALENT note below
ordering: runs BEFORE any CREATE2 in cloneAndInitialize C01 guard moved after the deploy new testCloneDeterministicCodeGuardRunsBeforeCreate2

cloneAndInitialize(address,bytes32,bytes,bytes32)

Behaviour Mutation Verdict Killing test
ordering: checkImplementationCode first, before any deploy C01 guard moved after the deploy new testCloneDeterministicCodeGuardRunsBeforeCreate2
builds creation code via cloneCreationCode(implementation) C02 built for address(this) pre testCloneDeterministicMatchesPredict
assembly ("memory-safe") create2(0, add(cc,0x20), mload(cc), derivedSalt) C05/C06/C07 each argument perturbed pre testCloneDeterministicMatchesPredict, testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt
create2 value literal 0 — no ETH forwarded to the clone C03 selfbalance(); C04 1 C03 new, C04 pre C03 testCloneDeterministicNoEthForwarded; C04 testCloneDeterministicMatchesPredict
memory offset add(creationCode, 0x20) skips the length word C05 offset is creationCode pre testCloneDeterministicMatchesPredict
length mload(creationCode) reads the length word (55) C06 sub(mload(creationCode), 1) pre testCloneDeterministicOpenSaltDoesNotConsumeNamespacedSalt
salt argument is the DERIVED salt, never the raw caller salt C07 raw salt pre testCloneDeterministicOpenSaltMatchesPredict
child == address(0) => revert CloneDeploymentFailed (typed) C08 guard inverted; C09 guard deleted pre C08 testCloneDeterministicOpenSaltCallerIndependent; C09 testCloneDeterministicSecondDeployReverts
an already-taken effective salt REVERTS rather than returning the existing clone C10 returns predictCloneAddress(...) instead of reverting pre testCloneDeterministicSecondDeployReverts
side-effect: emit NewClone(msg.sender, implementation, child, salt, data) C11 emit deleted pre testCloneDeterministicEvent
event carries the RAW caller salt, NOT derivedSalt C12 emits derivedSalt pre testCloneDeterministicEvent
event sender is msg.sender (not tx.origin, not address(this)) C13 tx.origin; C14 address(this) pre testCloneDeterministicEvent
event field count/order/types, all non-indexed C15 implementation/child swapped pre testCloneDeterministicEvent
event emitted BEFORE the initialize call C16 emit moved after the initialize check new testCloneDeterministicEventPrecedesInitialize
external call ICloneableV2(child).initialize(data), data forwarded verbatim C18 initialize(""); C19 initialize(abi.encode(data)) pre testCloneDeterministicMatchesPredict
atomicity: nothing else is called on the proxy before initialize C17 a discarded child.call("") inserted first new testCloneDeterministicInitializeIsTheOnlyCall
!= ICLONEABLE_V2_SUCCESS => revert InitializationFailed (typed) C20 comparison inverted; C21 return value unchecked; C22 wrong typed error; C23 a REVERTING initialize swallowed C20/C21/C22 pre, C23 new testCloneDeterministicInitializeFailureFails; C23 testCloneDeterministicInitializeRevertBubbles
return: the deployed child address C24 returns implementation pre testCloneDeterministicMatchesPredict

cloneDeterministic(address,bytes,bytes32)

Behaviour Mutation Verdict Killing test
derivation choice: effectiveSalt (namespaced), not effectiveOpenSalt D01 swap to the open-salt derivation; D02 namespace by tx.origin pre testCloneDeterministicSenderScoped

EQUIVALENT: B07, dropping view from checkImplementationCode

Recorded as equivalent, not forced to a kill. Dropping view alone changes no
runtime behaviour whatsoever: a non-view function that writes nothing executes
identically. The mutation is observable only at compile time, and the suite
already binds it there — testCheckImplementationCodeZero reaches the library
through an external view wrapper, so the mutant does not compile
(Error (8961): Function cannot be declared as view because this expression (potentially) modifies the state). The probe scores that NO-RUN, which is
correct: there is no suite run to score. No test was written to chase it,
because no test can distinguish a state the mutant cannot reach.

What the five new tests actually assert

Each one fails on its mutant and passes on real code, on a different observable
VALUE rather than a bare revert:

  • testCloneDeterministicCodeGuardRunsBeforeCreate2 — ordering is invisible
    unless both failure conditions hold at once, so the test builds that state: an
    ordinary deploy takes the effective salt, then vm.etch(implementation, "")
    strips the implementation's code. Guard first gives
    ZeroImplementationCodeSize; a guard after the deploy gives
    CloneDeploymentFailed. Two distinct typed errors, not one revert.
  • testCloneDeterministicNoEthForwarded — deals the factory a fuzzed
    balance (it has no payable surface, but can still receive by SELFDESTRUCT or
    as a withdrawal recipient) and asserts BOTH child.balance == 0 and the
    factory's balance unchanged, so neither "the clone got funded" nor "the factory
    got drained" passes.
  • testCloneDeterministicInitializeIsTheOnlyCallTestCloneable exposes
    only end state, so a call the factory made and discarded left no trace at all.
    The new TestCloneableCallRecorder appends the selector of every call the
    proxy receives, and the test asserts the whole recorded sequence is exactly
    [ICloneableV2.initialize.selector].
  • testCloneDeterministicEventPrecedesInitialize — the recorder logs from
    inside initialize, which is what makes relative order observable; the
    existing entries.length == 1 assertions pin a count and cannot see order.
    Asserts two entries, NewClone from the factory first, Initializing from the
    child second, both with full data.
  • testCloneDeterministicInitializeRevertBubbles — the suite had no
    implementation that REFUSES, only one that returns the wrong value. The new
    TestCloneableRevert reverts with a typed error carrying its data, and the
    test asserts that exact error with those exact arguments comes back out of the
    factory, plus the predicted address is left codeless.

Harness and evidence

Harness is the repo's own CI (.github/workflows/rainix-sol.yaml ->
rainix rainix-sol-test): forge soldeer install then forge test in
nix develop .#sol-shell.

$ forge test
Ran 6 test suites: 47 tests passed, 0 failed, 0 skipped (47 total tests)

$ forge fmt --check          # clean
$ reuse lint                 # compliant, 36/36 files
$ slither .                  # 8 contracts, 97 detectors, 0 results
$ forge snapshot             # .gas-snapshot regenerated

Attribution pass, pre-existing suite only, no test from this PR written yet:

baseline: green (42 passed)
== 32/38 killed; survived: 5; no-run: 1; harness errors: 0
C01 ordering: implementation-code guard moved AFTER the CREATE2: SURVIVED
C03 create2 forwards the factory's whole balance instead of the literal 0: SURVIVED
C16 NewClone emitted AFTER initialize instead of before: SURVIVED
C17 atomicity: something else is called on the proxy before initialize: SURVIVED
C23 a REVERTING initialize is swallowed instead of bubbling: SURVIVED

Targeted re-probe of exactly those five, with the new tests:

baseline: green (47 passed)
C01 ...: KILLED — killed by: testCloneDeterministicCodeGuardRunsBeforeCreate2(bytes32,bytes)
C03 ...: KILLED — killed by: testCloneDeterministicNoEthForwarded(bytes32,bytes,uint256)
C16 ...: KILLED — killed by: testCloneDeterministicEventPrecedesInitialize(bytes32,bytes)
C17 ...: KILLED — killed by: testCloneDeterministicInitializeIsTheOnlyCall(bytes32,bytes)
C23 ...: KILLED — killed by: testCloneDeterministicInitializeRevertBubbles(bytes32,bytes)
== 5/5 killed; survived: 0; no-run: 0; harness errors: 0

Full final pass, all 38 mutants:

baseline: green (47 passed)
== 37/38 killed; survived: 0; no-run: 1; harness errors: 0

The one NO-RUN is B07, the EQUIVALENT above. No mutant survives. The mutants
file is a probe artifact and is deliberately not committed.

Nothing was weakened

No existing test was deleted, weakened, or edited to pass under a mutation. The
one edit to an existing test adds a vm.assume so the fuzzer stops handing
vm.etch input the cheatcode refuses — it repairs a test that was red on
unmutated source, and the repaired test still kills B02, B04 and B05.

Unprobed

None. Every one of the 29 behaviours in the group has at least one mutant with a
recorded verdict.

QA

  • Discriminating tests: testCloneDeterministicCodeGuardRunsBeforeCreate2, testCloneDeterministicNoEthForwarded, testCloneDeterministicInitializeIsTheOnlyCall, testCloneDeterministicEventPrecedesInitialize, testCloneDeterministicInitializeRevertBubbles - each was verified to fail under its mutant and pass on unmutated source by mutation-probe, which applies the mutation, runs the whole suite, restores the file byte-exact and names the failing test. Each asserts a different observable VALUE, not a bare revert: two distinct typed errors (ZeroImplementationCodeSize vs CloneDeploymentFailed), both balances after a funded deploy, the whole recorded call-selector sequence on the proxy, two ordered log entries with full data, and the implementation's own error with its exact arguments.
  • Mutations applied:
    • src/lib/LibICloneableFactoryV4.sol:151-160 checkImplementationCode(implementation) moved below the child == address(0) block -> testCloneDeterministicCodeGuardRunsBeforeCreate2
    • src/lib/LibICloneableFactoryV4.sol:156 create2(0, …) -> create2(selfbalance(), …) -> testCloneDeterministicNoEthForwarded
    • src/lib/LibICloneableFactoryV4.sol:161 a discarded child.call("") inserted before the emit -> testCloneDeterministicInitializeIsTheOnlyCall
    • src/lib/LibICloneableFactoryV4.sol:161-166 emit NewClone(...) moved below the initialize check -> testCloneDeterministicEventPrecedesInitialize
    • src/lib/LibICloneableFactoryV4.sol:164-168 the initialize check replaced by a low-level call that swallows a revert -> testCloneDeterministicInitializeRevertBubbles
    • 33 further mutants across all 29 behaviours in the group were probed and are in the matrix above; 32 are killed by pre-existing tests and 1 (B07, dropping view) is uncompilable and recorded EQUIVALENT.
  • Oracle: the NatSpec is the oracle throughout, not the implementation. ICloneableFactoryV3.cloneDeterministic ("MUST call ICloneableV2.initialize atomically … MUST NOT call any other functions on the cloned proxy before initialize completes successfully … MUST ONLY consider the clone successfully created if initialize returns the keccak256 hash of the string ICloneableV2.initialize"), ICloneableFactoryV4 (both effective-salt derivations pinned to exact bytes, NewClone carrying the RAW caller salt), and the library's own docs for the guard-then-CREATE2-then-emit-then-initialize order and for each typed error. Expected values are computed independently of the library where a value is involved: the pre-existing prediction tests hold the line against OpenZeppelin Clones as a foreign EIP-1167 implementation and against the longhand CREATE2 formula over the EIP's literal bytes; the new tests assert selectors and log topic0 hashes derived from the interface signatures, and balances from vm.deal, never from the library's own arithmetic.
  • Category check: this PR is a mutation-coverage pass, not an issue fix, so there is no issue asking for categories. The scope it does claim is the 29-behaviour group g2-libicloneablefactoryv4-predi; all 29 are covered with a recorded verdict, none UNPROBED. Its first commit also repairs the pre-existing test defect reported in testCheckImplementationCodeEtched fails on unmutated main: vm.etch rejects fuzzed 0xEF-leading code #64 and testCheckImplementationCodeEtched fails on unmutated source when the fuzzer draws 0xEF-prefixed code #68. Refs InitializationFailed is not raised when the implementation does not implement ICloneableV2.initialize — caller gets bare 0x #61 (independently filed duplicate of cloneAndInitialize reverts with empty revert data, not InitializationFailed, when the clone's initialize returns nothing #72).

🤖 Generated with Claude Code

baku-ccron and others added 2 commits August 24, 2026 07:48
`testCheckImplementationCodeEtched` fuzzes arbitrary `code` bytes straight
into `vm.etch`. Any draw whose first byte is `0xef` is rejected by the
cheatcode itself — `0xef00` is the EOF magic and `0xef01` the EIP-7702
delegation magic, and an `0xef01…` blob that is not exactly the 23-byte
designator fails with `Eip7702 is not 23 bytes long`. The test therefore
failed on unmutated code for a harness reason, not because
`checkImplementationCode` did anything wrong, and did so only on the fuzz
seeds that happened to draw such a blob.

Assume the byte away so the test covers what it says it covers: any
installable non-empty code passes the size-only guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A mutation pass over LibICloneableFactoryV4 with the pre-existing suite left
five mutants alive, all in `cloneAndInitialize` — the shared
guard/CREATE2/emit/initialize/verify tail that only exists behind the two entry
points. Each gets a test that fails on the mutant and passes on the real code.

- Guard BEFORE the CREATE2. Only observable when both failure conditions hold
  at once, so the test builds that state: an ordinary deploy takes the
  effective salt, the implementation then loses its code, and the caller is
  told `ZeroImplementationCodeSize` rather than the `CloneDeploymentFailed` a
  guard placed after the deploy would report.
- `create2` value is a literal `0`. The factory is dealt a balance first — it
  has no payable surface but can still be a `SELFDESTRUCT` or withdrawal
  recipient — and both the clone's balance and the factory's are asserted.
- `initialize` is the first and only call on the fresh proxy, per
  "MUST NOT call any other functions on the cloned proxy before `initialize`".
  `TestCloneable` shows only end state, so `TestCloneableCallRecorder` records
  the selector of every call the proxy receives and the whole sequence is
  asserted.
- `NewClone` is emitted BEFORE `initialize` runs. The recorder logs from
  inside `initialize`, which is what makes the relative order visible; a count
  of one entry cannot see it.
- A reverting `initialize` bubbles verbatim rather than being swallowed, and
  leaves the predicted address codeless. `TestCloneableRevert` covers the
  refusal half of initialization failure; `TestCloneableFailure` already
  covered the wrong-return-value half.

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: 66ae48c4-30cf-408a-86df-198aaa2ecd1c

📥 Commits

Reviewing files that changed from the base of the PR and between c1c2afd and 9327e9a.

📒 Files selected for processing (10)
  • .gas-snapshot
  • test/concrete/TestCloneFactory.sol
  • test/concrete/TestCloneable.sol
  • test/concrete/TestCloneableCallRecorder.sol
  • test/concrete/TestCloneableFailure.sol
  • test/concrete/TestCloneableRevert.sol
  • test/src/concrete/TestCloneable.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.

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 2 commits August 24, 2026 12:26
# Conflicts:
#	.gas-snapshot
#	test/concrete/TestCloneableCallRecorder.sol
#	test/concrete/TestCloneableRevert.sol
#	test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol
#	test/src/lib/LibICloneableFactoryV4.cloneDeterministic.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:

- `TestCloneableCallRecorder` and `TestCloneableRevert` move to
  `test/concrete/`, out of the `test/src` mirror, and are the canonical
  versions — #76 carries the same two files byte for byte, having dropped its
  own `TestCloneableEmitter` and `TestCloneableReverter` duplicates, so the
  two branches merge without touching each other.
- Both now return the success sentinel as a LITERAL rather than importing
  `ICLONEABLE_V2_SUCCESS`, matching `TestCloneable` after the base branch: a
  fixture that imports the constant the library compares against moves in
  lockstep with it and cannot discriminate a drift.
- The `0xef` fuzz-domain narrowing on `testCheckImplementationCodeEtched` is
  dropped from this branch. All four AMT branches had patched that same line;
  it is a `main` defect and now lives there once.

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>
thedavidmeister pushed a commit that referenced this pull request Aug 24, 2026
…what they test

The merge from `2026-08-24-test-fixtures-out-of-src-mirror` already relocated
this branch's fixtures out of the `test/src` mirror, which only mirrors `src`.
This finishes the job.

Fixtures — three of this branch's five are gone, none of the coverage with them:

- `TestCloneableConformant` was `TestCloneable` plus a one-shot init guard and
  the typed overload. The base branch gives `TestCloneable` both, for the whole
  suite rather than for two tests, so the second fixture had nothing left to
  add. `testInitializeOnlyOnce` and `testTypedOverloadRevertsInitializeSignatureFn`
  move onto it unchanged apart from the error name.
- `TestCloneableReverter` and `TestCloneableEmitter` are replaced by
  `TestCloneableRevert` and `TestCloneableCallRecorder`, byte for byte the same
  files #78 carries, so the two branches merge without touching each other.
- `TestCloneableRevert` and `TestCloneableFailure` stay SEPARATE. A fixture
  that returns a non-success sentinel cannot also revert with a typed error
  carrying data; collapsing them would weaken
  `testInitializeRevertBubblesVerbatim` to a bare decode revert.

`testNothingCalledBeforeInitialize` gains a second, stronger oracle in the
swap. `TestCloneableEmitter` announced a stray call by emitting from its
fallback, so the whole assertion rode on a log count.
`TestCloneableCallRecorder` records the SELECTOR SEQUENCE instead, and the test
now asserts that exactly one call reached the clone and that it was
`initialize(bytes)`. Mutation-checked: a `child.call(...)` inserted into
`cloneAndInitialize` between `NewClone` and `initialize` is caught by the
selector assertion ("2 != 1") and is INVISIBLE to the log-count assertion,
because the recorder's fallback deliberately does not emit.

`TestCloneableV1Shaped` was imported by nothing on this branch — a fixture
built for a test that was never written. It now has it.
`testInitializeSelectorSharedWithV1` pins that V1 and V2 publish the SAME
`initialize(bytes)` selector, which is only half a hazard statement: the
question it raises is whether a V4 factory silently accepts a legacy
implementation and leaves a live clone that was never initialized. It does not
— the `bytes32` return decode finds an empty returndata buffer and reverts
before the sentinel comparison, with NO revert data, which the test asserts as
observed behaviour rather than assuming `InitializationFailed`.
Mutation-checked: give the fixture a `bytes32` return and the test fails.

Layout, so a file name says what it tests:

- `LibPublishedAbi.sol` is test support, not a mirror of any `src/lib` file, so
  it moves to `test/lib/`.
- `ICloneableFactoryV2.sol.t.sol` and `ICloneableFactoryV3.sol.t.sol` lose the
  doubled extension.
- `ICloneableV2.sol.t.sol` becomes `ICloneableV2.initialize.t.sol`, matching
  the `<unit>.<function>.t.sol` convention `test/src/lib` already uses. Every
  test in it is about `initialize` or its return sentinel.
- `DeprecatedInterfaces.t.sol` splits into `ICloneableV1.t.sol`,
  `ICloneableFactoryV1.t.sol` and `IFactory.t.sol`, one per file in
  `src/interface/deprecated/`, which is what the mirror is for. The three tests
  were independent and shared nothing but the notice, which each file keeps.

The `0xef` fuzz-domain narrowing this branch carried is resolved onto the base
branch's version of the same line, which now holds it once for all four AMT
branches instead of four times.

Gas snapshot regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baku-ccron and others added 2 commits August 24, 2026 12:56
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

Queuing a review — the original attempt hit the OSS rate limit before reading the diff, so this PR has never actually been reviewed.

Orientation: nothing under src/ is touched. This branch adopts the two shared fixtures, TestCloneableCallRecorder and TestCloneableRevert, which #76 carries byte for byte so the two merge without touching each other. Both return the success sentinel as a LITERAL rather than importing ICLONEABLE_V2_SUCCESS: a fixture that imports the constant the library compares against moves in lockstep with it and cannot discriminate a drift.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

@thedavidmeister I will review pull request #78. I will check the fixture behavior and the test assertions against the stated mutation-coverage scope.

⚠️ 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