diff --git a/README.md b/README.md index 786f341..d64834e 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,19 @@ against `METAMORPHIC_OPS` and returns the risky opcodes that are reachable. `checkNotMetamorphic` reverts with `Metamorphic(riskyOpcodes)` when that result is non-zero. +Bytecode whose first byte is `0xEF` fails closed: `scanMetamorphicRisk` reports +a bitmap of exactly `1 << 0xEF` without scanning, and `checkNotMetamorphic` +therefore reverts. EIP-3541 reserves that lead byte for protocol features — an +EOF container (`0xEF00`), an EIP-7702 delegation designator +(`0xEF0100 || address`, which the account holder repoints or revokes with one +transaction), or whatever the prefix is assigned next — that a legacy opcode +scan cannot reason about; pre-London deployments and chains without EIP-3541 can +hold `0xEF`-lead legacy code indistinguishable by inspection. The scan keys on +the first byte alone and refuses to vouch rather than misread the bytes as +opcodes. This makes the metamorphic pair total over bytes: it is the one place +that answers the reserved prefix with a verdict, while the raw opcode scans keep +reverting `EOFBytecodeNotSupported` on the `0xEF00` EOF magic. + Both functions also have address-taking entry points that read the account's code, revert with `CodelessAccount` when there is none, and delegate to the bytes functions otherwise. An account with no code is the maximally metamorphic diff --git a/src/interface/IExtrospectV1.sol b/src/interface/IExtrospectV1.sol index 5a1f8a4..d3fdf37 100644 --- a/src/interface/IExtrospectV1.sol +++ b/src/interface/IExtrospectV1.sol @@ -48,10 +48,13 @@ interface IExtrospectV1 { /// @param bytecode The bytecode to check. function checkNotEOFBytecode(bytes memory bytecode) external pure; - /// @notice Reverts `Metamorphic(riskyOpcodes)` when any metamorphic risk - /// opcode is reachable in `bytecode`, carrying the same bitmap - /// `scanMetamorphicRisk` returns. Reverts `EOFBytecodeNotSupported` when - /// `bytecode` is EOF. Returns nothing when that bitmap is zero. + /// @notice Reverts `Metamorphic(riskyOpcodes)` when `scanMetamorphicRisk` + /// reports a non-zero bitmap for `bytecode`, carrying that bitmap: any + /// reachable metamorphic risk opcode, or a first byte of the EIP-3541 + /// reserved `0xEF` (an EOF container, an EIP-7702 delegation designator, + /// or any future assignment of the prefix), which reverts + /// `Metamorphic(1 << 0xEF)`. Never reverts `EOFBytecodeNotSupported`. + /// Returns nothing when that bitmap is zero. /// @dev See `LibExtrospectMetamorphic.checkNotMetamorphic`. /// @param bytecode The bytecode to check. function checkNotMetamorphic(bytes memory bytecode) external pure; @@ -127,11 +130,15 @@ interface IExtrospectV1 { /// @notice Bitmap of the metamorphic risk opcodes /// (`SELFDESTRUCT`, `DELEGATECALL`, `CALLCODE`, `CREATE`, `CREATE2`) that /// `scanEVMOpcodesReachableInBytecode` finds reachable in `bytecode`. - /// Reverts `EOFBytecodeNotSupported` when `bytecode` is EOF. + /// Bytecode whose first byte is the EIP-3541 reserved `0xEF` — an EOF + /// container, an EIP-7702 delegation designator, or any future + /// assignment of the prefix — fails closed to a bitmap of exactly + /// `1 << 0xEF` instead of being scanned. Never reverts. /// @dev See `LibExtrospectMetamorphic.scanMetamorphicRisk`. /// @param bytecode The bytecode to scan. /// @return A bitmap, not a count or a score: bit `N` is set when - /// metamorphic opcode `N` is reachable. Zero when none are. + /// metamorphic opcode `N` is reachable, and bit `0xEF` alone is set when + /// the first byte is the reserved `0xEF`. Zero when neither holds. function scanMetamorphicRisk(bytes memory bytecode) external pure returns (uint256); /// @notice Removes the last 53 bytes of `bytecode` when they are the exact diff --git a/src/lib/EVMOpcodes.sol b/src/lib/EVMOpcodes.sol index c151102..0ff9a73 100644 --- a/src/lib/EVMOpcodes.sol +++ b/src/lib/EVMOpcodes.sol @@ -202,6 +202,9 @@ uint256 constant HALTING_BITMAP = (1 << EVM_OP_STOP) | (1 << EVM_OP_RETURN) | (1 /// non-zero result. Membership is by opcode alone and is not conditioned on /// whether the bytecode's own code can in fact be replaced, so bytecode that /// only deploys child contracts is rejected for CREATE or CREATE2. +/// Bytecode whose first byte is the EIP-3541 reserved `0xEF` never reaches +/// this mask: `scanMetamorphicRisk` fails closed on it first and reports +/// `1 << 0xEF`, a bit that is not a member here. //forge-lint: disable-next-line(incorrect-shift) uint256 constant METAMORPHIC_OPS = (1 << EVM_OP_SELFDESTRUCT) | (1 << EVM_OP_DELEGATECALL) //forge-lint: disable-next-line(incorrect-shift) diff --git a/src/lib/LibExtrospectMetamorphic.sol b/src/lib/LibExtrospectMetamorphic.sol index 9de90a9..9277b57 100644 --- a/src/lib/LibExtrospectMetamorphic.sol +++ b/src/lib/LibExtrospectMetamorphic.sol @@ -5,31 +5,71 @@ pragma solidity ^0.8.25; import {LibExtrospectBytecode} from "./LibExtrospectBytecode.sol"; import {METAMORPHIC_OPS} from "./EVMOpcodes.sol"; +/// @dev The lead byte EIP-3541 reserves: since the London hard fork no +/// ordinary deployment can produce code whose first byte is `0xEF`. On a +/// post-London chain new code beginning with it comes from protocol +/// features — an EOF container (`0xEF00`), an EIP-7702 delegation +/// designator (`0xEF0100 || address`), or whatever the prefix is assigned +/// next — none of which a legacy opcode scan can reason about; pre-London +/// deployments and chains without EIP-3541 can carry it as plain legacy +/// code the scan cannot tell apart by inspection. The metamorphic scan +/// fails closed on the first byte alone. +uint8 constant EIP3541_RESERVED_LEAD_BYTE = 0xEF; + /// @title LibExtrospectMetamorphic /// @notice Detection and guarding against metamorphic contract risk. Scans -/// bytecode for reachable opcodes in `METAMORPHIC_OPS`. The bytes-taking -/// entry points are total over bytes and answer only about the bytes given; -/// the address-taking entry points bind the verdict to an account and revert -/// with `CodelessAccount` rather than vouch for an account that has no code. +/// bytecode for reachable opcodes in `METAMORPHIC_OPS`, failing closed on +/// bytecode whose first byte is the EIP-3541 reserved `0xEF`: such code is a +/// protocol-defined format, not legacy opcodes, and the scan reports the +/// `0xEF` byte itself as the risky element rather than vouch for bytes it +/// cannot disassemble. The bytes-taking entry points are total over bytes and +/// answer only about the bytes given; the address-taking entry points bind +/// the verdict to an account and revert with `CodelessAccount` rather than +/// vouch for an account that has no code. library LibExtrospectMetamorphic { /// Thrown when metamorphic risk opcodes are reachable in bytecode. /// @param riskyOpcodes Bitmap of reachable metamorphic opcodes. error Metamorphic(uint256 riskyOpcodes); - /// Scans bytecode for reachable metamorphic risk opcodes. Reverts with - /// `EOFBytecodeNotSupported` if the bytecode is EOF. - /// Answers only about the bytes given. Use `scanMetamorphicRisk(address)` - /// to bind the scan to an account and reject a codeless one. + /// Scans bytecode for reachable metamorphic risk opcodes. Total over + /// bytes: never reverts, and answers only about the bytes given. Use + /// `scanMetamorphicRisk(address)` to bind the scan to an account and + /// reject a codeless one. + /// + /// Bytecode whose first byte is the EIP-3541 reserved `0xEF` fails + /// closed: the scan reports a bitmap of exactly `1 << 0xEF` before any + /// opcode scan, whatever the remaining bytes are. EIP-3541 reserves the + /// prefix for protocol features — an EOF container (`0xEF00`), an + /// EIP-7702 delegation designator (`0xEF0100 || address`, which the + /// account holder repoints or revokes with one transaction), or + /// whatever it is assigned next — that a legacy opcode scan cannot + /// reason about; pre-London deployments and chains without EIP-3541 + /// can hold `0xEF`-lead legacy code indistinguishable by inspection. + /// The verdict keys on the first byte alone and vouches for none of + /// it. The reported bit + /// is the `0xEF` lead byte itself, not a reachable opcode, and is not a + /// member of `METAMORPHIC_OPS`. The reserved prefix is the one first + /// byte that never reaches `scanEVMOpcodesReachableInBytecode`, so + /// `EOFBytecodeNotSupported` is never thrown here. /// @param bytecode The bytecode to scan. - /// @return riskyOpcodes Bitmap of reachable metamorphic opcodes. Zero if - /// no metamorphic risk opcodes are reachable, including when `bytecode` is - /// empty. - function scanMetamorphicRisk(bytes memory bytecode) internal pure returns (uint256 riskyOpcodes) { - riskyOpcodes = LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(bytecode) & METAMORPHIC_OPS; + /// @return Bitmap of risky elements: the reachable metamorphic opcodes, + /// or exactly `1 << 0xEF` when the first byte is the reserved `0xEF`. + /// Zero if no metamorphic risk opcodes are reachable, including when + /// `bytecode` is empty. + function scanMetamorphicRisk(bytes memory bytecode) internal pure returns (uint256) { + if (bytecode.length > 0 && uint8(bytecode[0]) == EIP3541_RESERVED_LEAD_BYTE) { + //forge-lint: disable-next-line(incorrect-shift) + return uint256(1) << EIP3541_RESERVED_LEAD_BYTE; + } + return LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(bytecode) & METAMORPHIC_OPS; } - /// Reverts if any metamorphic risk opcodes are reachable in bytecode. - /// Also reverts with `EOFBytecodeNotSupported` if the bytecode is EOF. + /// Reverts with `Metamorphic` when `scanMetamorphicRisk` reports a + /// nonzero bitmap for the bytecode: any reachable metamorphic risk + /// opcode, or a first byte of the EIP-3541 reserved `0xEF` — an EOF + /// container, an EIP-7702 delegation designator, or any future + /// assignment of the prefix — which reverts `Metamorphic(1 << 0xEF)`. + /// Never reverts `EOFBytecodeNotSupported`. /// Empty bytecode has no reachable metamorphic opcodes and does not revert, /// so an account with no code — an externally owned account, an unoccupied /// `CREATE2` target, or a self-destructed account — passes this check on @@ -52,12 +92,14 @@ library LibExtrospectMetamorphic { /// unoccupied `CREATE2` target, a self-destructed account between /// incarnations, or an EOA that can gain code by EIP-7702 delegation — /// and the chain cannot distinguish "can never gain code" from "can gain - /// anything", so a zero bitmap for it would vouch for nothing. Reverts - /// with `EOFBytecodeNotSupported` if the account's code is EOF, as the - /// bytes scan does. + /// anything", so a zero bitmap for it would vouch for nothing. Account + /// code whose first byte is the EIP-3541 reserved `0xEF` — an EOF + /// container or an EIP-7702 delegation designator — reports exactly + /// `1 << 0xEF`, as the bytes scan does. /// @param account The account whose code to scan. - /// @return riskyOpcodes Bitmap of reachable metamorphic opcodes in the - /// account's code. Zero if none are reachable. + /// @return Bitmap of risky elements in the account's code: the reachable + /// metamorphic opcodes, or exactly `1 << 0xEF` when the code's first + /// byte is the reserved `0xEF`. Zero if none are reachable. function scanMetamorphicRisk(address account) internal view returns (uint256) { bytes memory bytecode = account.code; if (bytecode.length == 0) { @@ -71,8 +113,9 @@ library LibExtrospectMetamorphic { /// `scanMetamorphicRisk(address)`. Reverts with `CodelessAccount` /// carrying the address when the account has no code, on the same terms /// as `scanMetamorphicRisk(address)`: no absence check answers "no code" - /// as a pass. Reverts with `EOFBytecodeNotSupported` if the account's - /// code is EOF. + /// as a pass. Reverts with `Metamorphic(1 << 0xEF)` when the account's + /// code begins with the EIP-3541 reserved `0xEF` — an EOF container or + /// an EIP-7702 delegation designator — as the bytes check does. /// @param account The account whose code to check. function checkNotMetamorphic(address account) internal view { uint256 riskyOpcodes = scanMetamorphicRisk(account); diff --git a/test/lib/LibExtrospectionSlow.sol b/test/lib/LibExtrospectionSlow.sol index 53a8408..2e1b157 100644 --- a/test/lib/LibExtrospectionSlow.sol +++ b/test/lib/LibExtrospectionSlow.sol @@ -40,6 +40,15 @@ uint256 constant SLOW_HALTING_BITMAP = //forge-lint: disable-next-line(incorrect-shift) | (uint256(1) << 0xFF); +/// @dev The lead byte EIP-3541 reserves: since the London hard fork no +/// ordinary deployment can produce code whose first byte is `0xEF`. New +/// post-London code beginning with it comes from a protocol feature — an +/// EOF container, an EIP-7702 delegation designator, or whatever the +/// prefix is assigned next — while pre-London deployments and chains +/// without EIP-3541 can carry it as plain legacy code; the slow scan +/// mirrors the first-byte-only fail-closed rule. +uint8 constant SLOW_EIP3541_LEAD_BYTE = 0xEF; + /// @dev Opcode bytes that indicate metamorphic risk: `SELFDESTRUCT`, /// `DELEGATECALL`, `CALLCODE`, `CREATE`, `CREATE2`. uint256 constant SLOW_METAMORPHIC_BITMAP = @@ -131,8 +140,14 @@ library LibExtrospectionSlow { return scan; } - /// KISS implementation of metamorphic risk scan. + /// KISS implementation of metamorphic risk scan. Fails closed on the + /// EIP-3541 reserved prefix: bytecode whose first byte is `0xEF` reports + /// that byte's bit alone, before any opcode scan. function scanMetamorphicRiskSlow(bytes memory data) internal pure returns (uint256) { + if (data.length > 0 && uint8(data[0]) == SLOW_EIP3541_LEAD_BYTE) { + //forge-lint: disable-next-line(incorrect-shift) + return uint256(1) << SLOW_EIP3541_LEAD_BYTE; + } return scanEVMOpcodesReachableInBytecodeSlow(data) & SLOW_METAMORPHIC_BITMAP; } diff --git a/test/src/lib/LibExtrospectMetamorphic.checkNotMetamorphic.t.sol b/test/src/lib/LibExtrospectMetamorphic.checkNotMetamorphic.t.sol index da3c383..13e0d91 100644 --- a/test/src/lib/LibExtrospectMetamorphic.checkNotMetamorphic.t.sol +++ b/test/src/lib/LibExtrospectMetamorphic.checkNotMetamorphic.t.sol @@ -122,12 +122,33 @@ contract LibExtrospectMetamorphicCheckNotMetamorphicTest is Test { this.checkNotMetamorphicExternal(code); } - /// EOF bytecode reverts with EOFBytecodeNotSupported. - function testCheckNotMetamorphicRevertsOnEOF() external { - vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); + /// EOF bytecode reverts with `Metamorphic` carrying the EIP-3541 + /// reserved `0xEF` lead byte's bit, not `EOFBytecodeNotSupported`. + function testCheckNotMetamorphicRevertsOnEOFReservedPrefix() external { + //forge-lint: disable-next-line(incorrect-shift) + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << 0xEF)); this.checkNotMetamorphicExternal(hex"EF00010203"); } + /// The EIP-7702 delegation designator `0xEF0100 || address` reverts with + /// `Metamorphic` carrying the EIP-3541 reserved `0xEF` lead byte's bit. + /// Inverts the repro on #54, which pinned a clean pass. + function testCheckNotMetamorphicRevertsOnEIP7702DelegationDesignator() external { + bytes memory designator = abi.encodePacked(hex"ef0100", address(0x1234567890123456789012345678901234567890)); + //forge-lint: disable-next-line(incorrect-shift) + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << 0xEF)); + this.checkNotMetamorphicExternal(designator); + } + + /// Fuzz: ANY bytecode whose first byte is `0xEF` reverts with + /// `Metamorphic(1 << 0xEF)`, whatever follows the first byte. + function testCheckNotMetamorphicReservedPrefixFuzz(bytes memory tail) external { + bytes memory bytecode = abi.encodePacked(hex"ef", tail); + //forge-lint: disable-next-line(incorrect-shift) + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << 0xEF)); + this.checkNotMetamorphicExternal(bytecode); + } + /// Address entry point: a codeless account reverts with `CodelessAccount` /// carrying the address. The bytes entry point passes the same account's /// (empty) code, pinned by `testCheckNotMetamorphicCodelessAccount`; only @@ -155,20 +176,34 @@ contract LibExtrospectMetamorphicCheckNotMetamorphicTest is Test { } /// Address entry point: EOF code etched onto an account reverts with - /// `EOFBytecodeNotSupported` via delegation to the bytes entry point. - function testCheckNotMetamorphicAddressRevertsOnEOF() external { + /// `Metamorphic` carrying the EIP-3541 reserved `0xEF` lead byte's bit, + /// via delegation to the bytes entry point. + function testCheckNotMetamorphicAddressRevertsOnEOFReservedPrefix() external { address target = address(0xBEEF); vm.etch(target, hex"EF00010203"); - vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); + //forge-lint: disable-next-line(incorrect-shift) + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << 0xEF)); this.checkNotMetamorphicAddressExternal(target); } - /// Fuzz: for an account with nonempty non-EOF code, the address entry + /// Address entry point: an account whose code is an EIP-7702 delegation + /// designator reverts with `Metamorphic` carrying the EIP-3541 reserved + /// `0xEF` lead byte's bit, via delegation to the bytes entry point. + function testCheckNotMetamorphicAddressRevertsOnEIP7702DelegationDesignator() external { + address target = address(0xBEEF); + vm.etch(target, abi.encodePacked(hex"ef0100", address(0x1234567890123456789012345678901234567890))); + //forge-lint: disable-next-line(incorrect-shift) + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << 0xEF)); + this.checkNotMetamorphicAddressExternal(target); + } + + /// Fuzz: for an account with nonempty etchable code, the address entry /// point and the bytes entry point agree: both revert with the same - /// `Metamorphic` bitmap or both pass. + /// `Metamorphic` bitmap or both pass. EOF code and EIP-7702 delegation + /// designators are included: both entry points revert `Metamorphic` with + /// `1 << 0xEF` for them. function testCheckNotMetamorphicAddressEquivalenceFuzz(bytes memory code) external { vm.assume(code.length > 0); - vm.assume(!LibExtrospectBytecode.isEOFBytecode(code)); address target = address(0xBEEF); LibExtrospectTestEtch.assumeEtch(vm, target, code); @@ -179,11 +214,10 @@ contract LibExtrospectMetamorphicCheckNotMetamorphicTest is Test { this.checkNotMetamorphicAddressExternal(target); } - /// Fuzz: checkNotMetamorphic reverts iff scanMetamorphicRisk is non-zero. + /// Fuzz: checkNotMetamorphic reverts iff scanMetamorphicRisk is + /// non-zero, over ALL bytes: the scan is total and the check adds only + /// the revert. function testCheckNotMetamorphicFuzz(bytes memory data) external { - // Skip EOF bytecode — both functions revert with a different error. - vm.assume(data.length < 2 || data[0] != 0xEF || data[1] != 0x00); - uint256 risk = LibExtrospectMetamorphic.scanMetamorphicRisk(data); if (risk != 0) { vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, risk)); diff --git a/test/src/lib/LibExtrospectMetamorphic.scanMetamorphicRisk.t.sol b/test/src/lib/LibExtrospectMetamorphic.scanMetamorphicRisk.t.sol index 1281448..23b2db1 100644 --- a/test/src/lib/LibExtrospectMetamorphic.scanMetamorphicRisk.t.sol +++ b/test/src/lib/LibExtrospectMetamorphic.scanMetamorphicRisk.t.sol @@ -26,11 +26,6 @@ import {NonMetamorphic} from "test/concrete/NonMetamorphic.sol"; import {LibExtrospectTestEtch} from "test/lib/LibExtrospectTestEtch.sol"; contract LibExtrospectMetamorphicScanMetamorphicRiskTest is Test { - /// External wrapper for EOF revert test. - function scanMetamorphicRiskExternal(bytes memory bytecode) external pure returns (uint256) { - return LibExtrospectMetamorphic.scanMetamorphicRisk(bytecode); - } - /// External wrapper for address entry point revert tests. function scanMetamorphicRiskAddressExternal(address account) external view returns (uint256) { return LibExtrospectMetamorphic.scanMetamorphicRisk(account); @@ -64,20 +59,32 @@ contract LibExtrospectMetamorphicScanMetamorphicRiskTest is Test { assertEq(risk, LibExtrospectMetamorphic.scanMetamorphicRisk(address(c).code)); } - /// Address entry point: EOF code etched onto an account reverts with - /// `EOFBytecodeNotSupported` via delegation to the bytes entry point. - function testScanMetamorphicRiskAddressRevertsOnEOF() external { + /// Address entry point: EOF code etched onto an account reports the + /// EIP-3541 reserved `0xEF` lead byte as the risky element, via delegation + /// to the bytes entry point. + function testScanMetamorphicRiskAddressEOFReservedPrefix() external { address target = address(0xBEEF); vm.etch(target, hex"EF00010203"); - vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); - this.scanMetamorphicRiskAddressExternal(target); + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(target), uint256(1) << 0xEF); + } + + /// Address entry point: an account whose code is an EIP-7702 delegation + /// designator reports the EIP-3541 reserved `0xEF` lead byte as the risky + /// element, via delegation to the bytes entry point. + function testScanMetamorphicRiskAddressEIP7702DelegationDesignator() external { + address target = address(0xBEEF); + vm.etch(target, abi.encodePacked(hex"ef0100", address(0x1234567890123456789012345678901234567890))); + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(target), uint256(1) << 0xEF); } - /// Fuzz: for an account with nonempty non-EOF code, the address entry + /// Fuzz: for an account with nonempty etchable code, the address entry /// point returns exactly what the bytes entry point returns for that code. + /// EOF code and EIP-7702 delegation designators are included: both entry + /// points report `1 << 0xEF` for them. function testScanMetamorphicRiskAddressEquivalenceFuzz(bytes memory code) external { vm.assume(code.length > 0); - vm.assume(!LibExtrospectBytecode.isEOFBytecode(code)); address target = address(0xBEEF); LibExtrospectTestEtch.assumeEtch(vm, target, code); @@ -194,16 +201,67 @@ contract LibExtrospectMetamorphicScanMetamorphicRiskTest is Test { assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(hex"61F4"), 0); } - /// Fuzz test against slow reference. + /// Fuzz test against slow reference, total over all bytes: the scan + /// never reverts, including on the EIP-3541 reserved `0xEF` prefix. function testScanMetamorphicRiskReference(bytes memory data) external pure { - vm.assume(!LibExtrospectBytecode.isEOFBytecode(data)); assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(data), LibExtrospectionSlow.scanMetamorphicRiskSlow(data)); } - /// EOF bytecode reverts. - function testScanMetamorphicRiskRevertsOnEOF() external { - vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); - this.scanMetamorphicRiskExternal(hex"EF00010203"); + /// EOF bytecode reports the EIP-3541 reserved `0xEF` lead byte as the + /// risky element instead of reverting `EOFBytecodeNotSupported`. + function testScanMetamorphicRiskEOFReservedPrefix() external pure { + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(hex"EF00010203"), uint256(1) << 0xEF); + } + + /// The EIP-7702 delegation designator `0xEF0100 || address` reports the + /// EIP-3541 reserved `0xEF` lead byte as the risky element. The account + /// holder can repoint or revoke the delegation with one transaction, so + /// the code at the account is the live "different code at the same + /// address" case. Inverts the repro on #54, which pinned a zero scan. + /// `isEOFBytecode` stays `false` for the designator per #53: the gate + /// lives in the metamorphic scan, not in the EOF predicate. + function testScanMetamorphicRiskEIP7702DelegationDesignator() external pure { + bytes memory designator = abi.encodePacked(hex"ef0100", address(0x1234567890123456789012345678901234567890)); + assertEq(designator.length, 23); + assertFalse(LibExtrospectBytecode.isEOFBytecode(designator)); + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(designator), uint256(1) << 0xEF); + } + + /// The designator verdict does not depend on the delegate address's hex + /// digits. This delegate spells `JUMPDEST DELEGATECALL` in its leading + /// bytes, which previously resumed the legacy scan inside the address and + /// flipped the verdict to `1 << DELEGATECALL`; now every designator + /// reports the same `0xEF` bit. + function testScanMetamorphicRiskEIP7702DelegationDesignatorDelegateIndependent() external pure { + bytes memory designator = abi.encodePacked(hex"ef0100", address(0x5BF4000000000000000000000000000000000000)); + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(designator), uint256(1) << 0xEF); + } + + /// A single bare `0xEF` byte reports itself as the risky element: the + /// fail-closed rule is the first byte alone, not any longer prefix shape. + function testScanMetamorphicRiskBareReservedByte() external pure { + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(hex"EF"), uint256(1) << 0xEF); + } + + /// An EOF container of a future version (`0xEF02...`) reports the + /// EIP-3541 reserved `0xEF` lead byte as the risky element: the rule + /// covers every future assignment of the prefix, not a registry of known + /// formats. + function testScanMetamorphicRiskFutureReservedPrefix() external pure { + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(hex"EF02010203"), uint256(1) << 0xEF); + } + + /// Fuzz: ANY bytecode whose first byte is `0xEF` scans to exactly + /// `1 << 0xEF`, whatever follows the first byte. + function testScanMetamorphicRiskReservedPrefixFuzz(bytes memory tail) external pure { + bytes memory bytecode = abi.encodePacked(hex"ef", tail); + //forge-lint: disable-next-line(incorrect-shift) + assertEq(LibExtrospectMetamorphic.scanMetamorphicRisk(bytecode), uint256(1) << 0xEF); } /// `hex"00F0"` is STOP followed by CREATE. CREATE is a metamorphic op but is