Skip to content

testCheckImplementationCodeEtched fails on unmutated main: vm.etch rejects fuzzed 0xEF-leading code #64

Description

@thedavidmeister

Unit

test/src/lib/LibICloneableFactoryV4.checkImplementationCode.t.sol — testCheckImplementationCodeEtched, at c1c2afd.

Intent oracle

The test's own docstring:

Any nonempty code is enough to pass: the guard is a code-size check, not a validation of what the code is.

That is the correct property for LibICloneableFactoryV4.checkImplementationCode (src/lib/LibICloneableFactoryV4.sol:130-134), which only reads implementation.code.length. The library is not at fault here — the defect is in the test.

Violated property

The test fails on unmutated main. It is not a rare flake: run in isolation it fails on every attempt.

function testCheckImplementationCodeEtched(address implementation, bytes memory code) external {
    vm.assume(implementation.code.length == 0);
    vm.assume(uint160(implementation) > 0x0a);
    vm.assume(code.length > 0);
    vm.etch(implementation, code);          // <- rejects some fuzzed `code`
    LibICloneableFactoryV4.checkImplementationCode(implementation);
}

vm.etch refuses to write runtime code whose first byte is 0xEF: a 0xef01… prefix is parsed as an EIP-7702 delegation designator and rejected unless it is exactly 23 bytes. The fuzzer reaches such a code value readily, and the failure is a cheatcode error, not an assertion — so it says nothing about the guard under test.

Note this excluded region is unreachable on chain anyway: EIP-3541 has rejected 0xEF-leading contract code since London. So the fuzz domain is strictly larger than the domain the guard is specified over.

Verified repro

$ forge test --match-test testCheckImplementationCodeEtched     # x8, no seed pinned
[FAIL: vm.etch: failed to create bytecode: Eip7702 is not 23 bytes long;
 counterexample: args=[0x9E5109B02f37848c268f4B2b1CDDeDEc5c7de965,
 0xef0137773327608555a081c828d215a508a7a36aef649793aaa00f6641e9e5501dae617bd8611af98132fc74951c]]
 testCheckImplementationCodeEtched(address,bytes) (runs: 30, ...)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped

8 out of 8 runs failed, at run counts between 27 and 49.

Whole-suite runs (forge test) pass more often, because the fuzzer's dictionary differs, but not reliably. Under mutation probing this test surfaced as a spurious "killer" of mutants that the suite does not actually detect — including both ICLONEABLE_V2_SUCCESS mutants, which genuinely survive. That is the concrete harm: a flaky pass/fail here silently inflates the apparent coverage of everything else.

Since foundry.toml pins no [fuzz] seed, the seed is fresh per run and CI will fail intermittently.

Proposed fix

Narrow the fuzz domain to code that can exist at an address:

vm.assume(code.length > 0);
// EIP-3541 makes `0xEF`-leading runtime code unreachable on chain, and
// `vm.etch` refuses to write it — a `0xef01` prefix is parsed as an EIP-7702
// delegation designator and rejected with "Eip7702 is not 23 bytes long".
vm.assume(code[0] != 0xef);
vm.etch(implementation, code);

With this applied, forge test was green 6/6 consecutive runs, and mutation probing became stable (the spurious kills disappear and the two real ICLONEABLE_V2_SUCCESS survivors are reported consistently).

This fix is applied in the AMT PR for group g3-libicloneablefactoryv4-clone, which needed a trustworthy baseline before it could probe anything. Filing separately because it is a pre-existing defect on main, independent of that PR, and because narrowing a fuzz domain deserves review on its own terms — an alternative worth considering is pinning a [fuzz] seed in foundry.toml, which would make the failure deterministic rather than removing it.

Triage framing

Flagging, not adjudicating. I am confident the test is broken as written; I am not the one to choose between the vm.assume narrowing above, a seed pin, or restructuring the test to etch from a fixed set of code shapes rather than fuzzed bytes.

Found during AMT group g3-libicloneablefactoryv4-clone, step 1 (baseline).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    adversarialFound by adversarial reviewauditAudit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions