Found by an adversarial mutation-test pass over LibICloneableFactoryV4
(group g2-libicloneablefactoryv4-predi), scanned at
c1c2afd. Filed for triage, not adjudicated.
Unit
LibICloneableFactoryV4.checkImplementationCode,
src/lib/LibICloneableFactoryV4.sol:130-134.
function checkImplementationCode(address implementation) internal view {
if (implementation.code.length == 0) {
revert ZeroImplementationCodeSize();
}
}
Intent oracle
ZeroImplementationCodeSize NatSpec: "Thrown when an implementation has zero
code size which is always a mistake: an EIP-1167 proxy of a codeless
implementation delegates every call — initialize included — to nothing."
ICloneableFactoryV4.predictDeterministicAddressOpenSalt: "A non-zero code
size at the returned address means this exact (implementation, data, salt)
has already been deployed by somebody … what occupies it is the clone that
was asked for, initialized with the bytes that were asked for."
Violated property
An EOA carrying an EIP-7702 delegation designator has EXTCODESIZE == 23 — the
23 bytes are 0xef0100 ++ delegate, not implementation code — so it passes the
guard, and a clone of it works: the EVM resolves the designator on
DELEGATECALL, so initialize runs the delegate's code against the clone's
storage.
That is a category the guard's stated rationale does not cover. A contract's
code is immutable for the life of the account; a delegation designator is not.
The EOA holder can sign a new authorization pointing at different code, or
revoke to empty, at any time and after clones already exist. The state the
guard exists to prevent — "delegates every call to nothing" — is reachable
after the guard has run, on every clone ever made against that implementation,
by a party who is not the deployer and not the clone's users.
It also touches the open-salt argument quoted above. That argument holds
(factory, implementation, salt, data) fixed and concludes that whatever
occupies the predicted address is the intended contract initialized with the
intended bytes. With a delegated EOA as implementation the address still
commits to those four things, but what the clone does is whatever the EOA
holder last delegated to, which is outside the tuple, and can change after the
address is pinned. Two callers reaching "the same" open-salt clone at different
times can be initialized by different code.
Verified repro
test/src/lib/Probe7702.t.sol, run with forge test --match-contract Probe7702 -vv:
// SPDX-License-Identifier: LicenseRef-DCL-1.0
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;
import {Test} from "forge-std-1.16.1/src/Test.sol";
import {LibICloneableFactoryV4} from "src/lib/LibICloneableFactoryV4.sol";
import {TestCloneFactory} from "test/src/concrete/TestCloneFactory.sol";
import {TestCloneable} from "test/src/concrete/TestCloneable.sol";
contract Probe7702Test is Test {
function testProbe7702() external {
TestCloneFactory factory = new TestCloneFactory();
TestCloneable delegate = new TestCloneable();
address eoa = address(uint160(uint256(keccak256("eoa"))));
// The EIP-7702 delegation designator an authorization installs.
vm.etch(eoa, abi.encodePacked(hex"ef0100", delegate));
emit log_named_uint("designator code length", eoa.code.length);
LibICloneableFactoryV4.checkImplementationCode(eoa);
emit log_string("guard PASSED for the 7702 designator");
address child = factory.cloneDeterministic(eoa, hex"1234", bytes32(0));
emit log_named_address("clone of a 7702 EOA deployed", child);
emit log_named_bytes("sData", TestCloneable(child).sData());
}
}
Observed:
designator code length: 23
guard PASSED for the 7702 designator
clone of a 7702 EOA deployed: 0xb2606d421b070a17d125b143C11B72879337DB24
sData: 0x1234
Triage framing
Readings, none of them chosen here:
- Out of the guard's remit, docs need a line. The guard is deliberately
size-only ("the guard validates SIZE, not content"), and no EXTCODESIZE
check can bind an account's future code — a self-destructing implementation
pre-Cancun raised the same question. On this reading nothing in src/
changes and the ZeroImplementationCodeSize NatSpec gains a sentence saying
that a non-zero code size is checked at deploy time only, and that a
delegated EOA is accepted with mutable, revocable behaviour.
- Cheap guard worth having. Rejecting an implementation whose code begins
with 0xef01 and is exactly 23 bytes long is three opcodes' worth of check
and would keep the "clone delegates to real, immutable code" property the
error's rationale asserts. Costs bytecode on the deploy path and rules out a
use somebody may want deliberately.
- Consumer-side note only. The address-derivation guarantees on
ICloneableFactoryV4 may want an explicit "the implementation must be a
contract, not a delegated EOA, for the pinned-address argument to mean what
it says".
Not adjudicated: whether a delegated EOA is a legitimate implementation is a
protocol decision, not something the test pass can settle.
Found by an adversarial mutation-test pass over
LibICloneableFactoryV4(group
g2-libicloneablefactoryv4-predi), scanned atc1c2afd. Filed for triage, not adjudicated.
Unit
LibICloneableFactoryV4.checkImplementationCode,src/lib/LibICloneableFactoryV4.sol:130-134.Intent oracle
ZeroImplementationCodeSizeNatSpec: "Thrown when an implementation has zerocode size which is always a mistake: an EIP-1167 proxy of a codeless
implementation delegates every call —
initializeincluded — to nothing."ICloneableFactoryV4.predictDeterministicAddressOpenSalt: "A non-zero codesize at the returned address means this exact
(implementation, data, salt)has already been deployed by somebody … what occupies it is the clone that
was asked for, initialized with the bytes that were asked for."
Violated property
An EOA carrying an EIP-7702 delegation designator has
EXTCODESIZE == 23— the23 bytes are
0xef0100 ++ delegate, not implementation code — so it passes theguard, and a clone of it works: the EVM resolves the designator on
DELEGATECALL, soinitializeruns the delegate's code against the clone'sstorage.
That is a category the guard's stated rationale does not cover. A contract's
code is immutable for the life of the account; a delegation designator is not.
The EOA holder can sign a new authorization pointing at different code, or
revoke to empty, at any time and after clones already exist. The state the
guard exists to prevent — "delegates every call to nothing" — is reachable
after the guard has run, on every clone ever made against that implementation,
by a party who is not the deployer and not the clone's users.
It also touches the open-salt argument quoted above. That argument holds
(factory, implementation, salt, data)fixed and concludes that whateveroccupies the predicted address is the intended contract initialized with the
intended bytes. With a delegated EOA as
implementationthe address stillcommits to those four things, but what the clone does is whatever the EOA
holder last delegated to, which is outside the tuple, and can change after the
address is pinned. Two callers reaching "the same" open-salt clone at different
times can be initialized by different code.
Verified repro
test/src/lib/Probe7702.t.sol, run withforge test --match-contract Probe7702 -vv:Observed:
Triage framing
Readings, none of them chosen here:
size-only ("the guard validates SIZE, not content"), and no
EXTCODESIZEcheck can bind an account's future code — a self-destructing implementation
pre-Cancun raised the same question. On this reading nothing in
src/changes and the
ZeroImplementationCodeSizeNatSpec gains a sentence sayingthat a non-zero code size is checked at deploy time only, and that a
delegated EOA is accepted with mutable, revocable behaviour.
with
0xef01and is exactly 23 bytes long is three opcodes' worth of checkand would keep the "clone delegates to real, immutable code" property the
error's rationale asserts. Costs bytecode on the deploy path and rules out a
use somebody may want deliberately.
ICloneableFactoryV4may want an explicit "the implementation must be acontract, not a delegated EOA, for the pinned-address argument to mean what
it says".
Not adjudicated: whether a delegated EOA is a legitimate implementation is a
protocol decision, not something the test pass can settle.