From d7b28e176d2a2d42842ce77e1c9c4688996e82f3 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 19 Aug 2026 14:19:05 +0000 Subject: [PATCH 1/3] Failing tests: IExtrospectV2 address entry points reject codeless accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #139 (ruled 2026-08-19 on #83): a new interface for a new deploy. IExtrospectV1 is frozen in practice — its concrete is deterministically deployed on all seven supported networks and can never gain functions — so the address-taking verdict surface that exists at the library level (the metamorphic pair from PR #136, the opcode-scan pair from PR #141, the any-0xEF fail-closed ruling from #54 / PR #138) lands on a new IExtrospectV2. This commit adds src/interface/IExtrospectV2.sol — the whole V1 surface unchanged plus checkNotMetamorphic(address), scanMetamorphicRisk(address), scanEVMOpcodesPresentInBytecode(address) and scanEVMOpcodesReachableInBytecode(address), types-only returns per org convention — and the binding tests, plus the minimal fixture they need to compile: ExtrospectV2Fixture forwards every interface function to the library function it names, except the four address-taking entry points deliberately forward through the BYTES functions over account.code, without the codeless boundary, so the discriminating tests fail behaviourally: - testIExtrospectV2CheckNotMetamorphicAddressRevertsOnCodelessAccount - testIExtrospectV2ScanMetamorphicRiskAddressRevertsOnCodelessAccount - testIExtrospectV2ScanPresentAddressRevertsOnCodelessAccount - testIExtrospectV2ScanReachableAddressRevertsOnCodelessAccount nix develop -c forge test: 413 passed, 7 failed — the 4 above plus the 3 fork tests needing ARBITRUM_RPC_URL (environmental, fail on main too). Also pins through the interface, passing already against the scaffold: selector identity of the carried surface with IExtrospectV1, the V1-ABI answers of the four overloaded names, agreement of address entry points with bytes entry points over deployed and etched code (fixed and fuzzed), the EIP-7702 designator and EOF-code fail-closed Metamorphic(1 << 0xEF) verdicts, and the split between the raw scans' 0xEF00-only EOF gate and the metamorphic pair's any-0xEF fail-closed rule. Co-Authored-By: Claude Fable 5 --- src/interface/IExtrospectV2.sol | 239 ++++++++++++++++ test/concrete/ExtrospectV2Fixture.sol | 101 +++++++ test/src/interface/IExtrospectV2.t.sol | 367 +++++++++++++++++++++++++ 3 files changed, 707 insertions(+) create mode 100644 src/interface/IExtrospectV2.sol create mode 100644 test/concrete/ExtrospectV2Fixture.sol create mode 100644 test/src/interface/IExtrospectV2.t.sol diff --git a/src/interface/IExtrospectV2.sol b/src/interface/IExtrospectV2.sol new file mode 100644 index 0000000..3fd0dd6 --- /dev/null +++ b/src/interface/IExtrospectV2.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity ^0.8.25; + +/// @title IExtrospectV2 +/// @notice External interface to the extrospection libraries, for a new +/// deploy. Every function forwards to the library function it names. The +/// concrete implementation, its deploy record and its release live in +/// rain.extrospection.deploy. +/// +/// `IExtrospectV1` is frozen in practice: the concrete implementing it is +/// deterministically deployed and can never gain functions. V2 therefore +/// carries the whole V1 surface unchanged, so a concrete bound to this one +/// interface exposes every library entry point, and adds the address-taking +/// verdict entry points that exist at the library level: +/// `checkNotMetamorphic(address)`, `scanMetamorphicRisk(address)`, +/// `scanEVMOpcodesPresentInBytecode(address)` and +/// `scanEVMOpcodesReachableInBytecode(address)`. 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 +/// `CodelessAccount(account)` rather than vouch for an account that has no +/// code, which can gain any code later. +/// @dev The custom errors named below are declared by the libraries, not by +/// this interface. `EOFBytecodeNotSupported`, `MetadataNotTrimmed`, +/// `BytecodeHashMismatch`, `UnexpectedMetadata` and `CodelessAccount` come +/// from `LibExtrospectBytecode`; `Metamorphic` comes from +/// `LibExtrospectMetamorphic`. +interface IExtrospectV2 { + /// @notice Reads `account`'s runtime bytecode, trims trailing Solidity CBOR + /// metadata from it, and reverts unless what remains hashes to + /// `expectedTrimmedHash`. Reverts `MetadataNotTrimmed` when the bytecode + /// does not end in the exact metadata structure the trimmer recognises, + /// so nothing was trimmed. Reverts + /// `BytecodeHashMismatch(expectedTrimmedHash, actual)` when the trimmed + /// bytecode hashes to something other than `expectedTrimmedHash`. Reverts + /// `EOFBytecodeNotSupported` when `account`'s bytecode is EOF. Returns + /// nothing when the hash matches. + /// @dev See `LibExtrospectBytecode.checkCBORTrimmedBytecodeHash`. + /// @param account The account whose runtime bytecode is read and checked. + /// @param expectedTrimmedHash `keccak256` of the bytecode AFTER its last + /// 53 bytes are removed, not of the full runtime bytecode. Not the same + /// value as `isBeaconImplementationBytecode`'s `expectedRuntimeHash`, + /// which hashes runtime bytecode whole. + function checkCBORTrimmedBytecodeHash(address account, bytes32 expectedTrimmedHash) external view; + + /// @notice Reads `account`'s runtime bytecode and reverts + /// `UnexpectedMetadata` when its last 53 bytes are the exact Solidity CBOR + /// metadata structure `tryTrimSolidityCBORMetadata` recognises. Reverts + /// `CodelessAccount(account)` when `account` has no code: absence of code + /// is not absence of metadata risk, so the check refuses to vouch for a + /// codeless account. Reverts `EOFBytecodeNotSupported` when `account`'s + /// bytecode is EOF. Returns nothing when the account has code in which no + /// such metadata is detected, including metadata in any other shape. + /// @dev See `LibExtrospectBytecode.checkNoSolidityCBORMetadata`. + /// @param account The account whose runtime bytecode is read and checked. + function checkNoSolidityCBORMetadata(address account) external view; + + /// @notice Reverts `EOFBytecodeNotSupported` when `bytecode` begins with + /// the EOF magic `0xEF00`. Returns nothing otherwise. + /// @dev See `LibExtrospectBytecode.checkNotEOFBytecode`. + /// @param bytecode The bytecode to check. + function checkNotEOFBytecode(bytes memory bytecode) external pure; + + /// @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, including for empty + /// `bytecode`: whether an account has any code at all is not checked + /// here. Use `checkNotMetamorphic(address)` to bind the verdict to an + /// account and reject a codeless one. + /// @dev See `LibExtrospectMetamorphic.checkNotMetamorphic`. + /// @param bytecode The bytecode to check. + function checkNotMetamorphic(bytes memory bytecode) external pure; + + /// @notice Reads `account`'s code and reverts `CodelessAccount(account)` + /// when there is none: an account with no code can gain any code later — + /// an unoccupied `CREATE2` target, a self-destructed account between + /// incarnations, or an EOA that can gain code by EIP-7702 delegation — + /// so no absence check answers "no code" as a pass. Otherwise reverts + /// `Metamorphic(riskyOpcodes)` exactly when `checkNotMetamorphic(bytes)` + /// does for the account's code: any reachable metamorphic risk opcode, + /// or code whose first byte is 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 the + /// account has code in which no metamorphic risk is found. + /// @dev See `LibExtrospectMetamorphic.checkNotMetamorphic`. + /// @param account The account whose code is read and checked. + function checkNotMetamorphic(address account) external view; + + /// @notice Static-calls `implementation()` on `beacon` and hashes the + /// runtime bytecode of the address it returns. + /// @dev See `LibExtrospectERC1967BeaconProxy.isBeaconImplementationBytecode`. + /// @param beacon The address to static-call `implementation()` on. + /// @param expectedRuntimeHash `keccak256` of the implementation's runtime + /// bytecode exactly as deployed, with no metadata trimming. Not the same + /// value as `checkCBORTrimmedBytecodeHash`'s `expectedTrimmedHash`, which + /// hashes runtime bytecode with its metadata trailer removed. For an + /// implementation with no code this is `keccak256` of empty bytes. + /// @return True when the static call succeeds, returns exactly 32 bytes + /// whose top 12 bytes are zero, and the address in those bytes has runtime + /// bytecode hashing to `expectedRuntimeHash`. False when the static call + /// reverts, returns other than 32 bytes, or returns 32 bytes with any of + /// the top 12 non-zero. + function isBeaconImplementationBytecode(address beacon, bytes32 expectedRuntimeHash) external view returns (bool); + + /// @notice Static-calls `owner()` on `beacon` and compares the address it + /// returns against `expectedOwner`. + /// @dev See `LibExtrospectERC1967BeaconProxy.isBeaconOwner`. + /// @param beacon The address to static-call `owner()` on. + /// @param expectedOwner The address the call must return. + /// @return True when the static call succeeds, returns exactly 32 bytes + /// whose top 12 bytes are zero, and the address in those bytes equals + /// `expectedOwner`. False when the static call reverts, returns other than + /// 32 bytes, or returns 32 bytes with any of the top 12 non-zero. + function isBeaconOwner(address beacon, address expectedOwner) external view returns (bool); + + /// @notice Whether `bytecode` begins with the EOF magic `0xEF00`. Never + /// reverts. Bytecode shorter than 2 bytes is not EOF. + /// @dev See `LibExtrospectBytecode.isEOFBytecode`. + /// @param bytecode The bytecode to check. + /// @return True when the first two bytes of `bytecode` are `0xEF00`. + function isEOFBytecode(bytes memory bytecode) external pure returns (bool); + + /// @notice Whether `bytecode` is the 45 byte ERC-1167 minimal proxy, and + /// the implementation address embedded in it when it is. Never reverts, + /// and performs no EOF check. + /// @dev See `LibExtrospectERC1167Proxy.isERC1167Proxy`. + /// @param bytecode The bytecode to check. + /// @return True when `bytecode` is exactly 45 bytes and carries the + /// ERC-1167 prefix and suffix. + /// @return The 20 bytes at offset 10 of `bytecode` read as an address, or + /// the zero address when the first return is false. + function isERC1167Proxy(bytes memory bytecode) external pure returns (bool, address); + + /// @notice Bitmap of every opcode byte a linear scan of `bytecode` reads, + /// skipping the inline data of `PUSH*` opcodes. Regions that never execute, + /// such as trailing metadata, still set bits. Reverts + /// `EOFBytecodeNotSupported` when `bytecode` is EOF. + /// @dev See `LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode`. + /// @param bytecode The bytecode to scan. + /// @return A bitmap, not a count: bit `N` is set when opcode `N` was read. + function scanEVMOpcodesPresentInBytecode(bytes memory bytecode) external pure returns (uint256); + + /// @notice Reads `account`'s code and reverts `CodelessAccount(account)` + /// when there is none: the empty code of a codeless account scans to a + /// zero bitmap, and zero says nothing about what opcodes that account may + /// later gain, so the scan refuses to vouch for it. Otherwise the bitmap + /// `scanEVMOpcodesPresentInBytecode(bytes)` reports for the account's + /// code, with the same `EOFBytecodeNotSupported` revert when that code is + /// EOF. + /// @dev See `LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode`. + /// @param account The account whose code is read and scanned. + /// @return A bitmap, not a count: bit `N` is set when opcode `N` was read + /// in the account's code. + function scanEVMOpcodesPresentInBytecode(address account) external view returns (uint256); + + /// @notice Bitmap of the opcodes a linear scan of `bytecode` treats as + /// reachable. The scan skips the inline data of `PUSH*` opcodes, pauses at + /// each halting opcode (`STOP`, `JUMP`, `RETURN`, `REVERT`, `INVALID`, + /// `SELFDESTRUCT`) and resumes at the next `JUMPDEST`, so bytes between a + /// halt and the next `JUMPDEST` set no bits. Reachability is + /// over-approximated: a `JUMPDEST` that no execution path can reach still + /// resumes the scan. Reverts `EOFBytecodeNotSupported` when `bytecode` is + /// EOF. + /// @dev See `LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode`. + /// @param bytecode The bytecode to scan. + /// @return A bitmap, not a count: bit `N` is set when opcode `N` was + /// scanned as reachable. + function scanEVMOpcodesReachableInBytecode(bytes memory bytecode) external pure returns (uint256); + + /// @notice Reads `account`'s code and reverts `CodelessAccount(account)` + /// when there is none: the empty code of a codeless account scans to a + /// zero bitmap, and zero says nothing about what opcodes that account may + /// later gain, so the scan refuses to vouch for it. Otherwise the bitmap + /// `scanEVMOpcodesReachableInBytecode(bytes)` reports for the account's + /// code, with the same `EOFBytecodeNotSupported` revert when that code is + /// EOF. + /// @dev See `LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode`. + /// @param account The account whose code is read and scanned. + /// @return A bitmap, not a count: bit `N` is set when opcode `N` was + /// scanned as reachable in the account's code. + function scanEVMOpcodesReachableInBytecode(address account) external view returns (uint256); + + /// @notice Bitmap of the metamorphic risk opcodes + /// (`SELFDESTRUCT`, `DELEGATECALL`, `CALLCODE`, `CREATE`, `CREATE2`) that + /// `scanEVMOpcodesReachableInBytecode` finds reachable in `bytecode`. + /// 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. Empty `bytecode` + /// scans to zero: whether an account has any code at all is not checked + /// here. Use `scanMetamorphicRisk(address)` to bind the scan to an + /// account and reject a codeless one. + /// @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, 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 Reads `account`'s code and reverts `CodelessAccount(account)` + /// when there is none: an account with no code is the maximally + /// metamorphic state — an unoccupied `CREATE2` target, a self-destructed + /// account between incarnations, or an EOA that can gain code by + /// EIP-7702 delegation — so a zero bitmap for it would vouch for + /// nothing. Otherwise the bitmap `scanMetamorphicRisk(bytes)` reports + /// for the account's code: the reachable metamorphic risk opcodes, or + /// exactly `1 << 0xEF` when that code's first byte is the EIP-3541 + /// reserved `0xEF` — an EOF container, an EIP-7702 delegation + /// designator, or any future assignment of the prefix. Never reverts + /// `EOFBytecodeNotSupported`. + /// @dev See `LibExtrospectMetamorphic.scanMetamorphicRisk`. + /// @param account The account whose code is read and scanned. + /// @return A bitmap, not a count or a score: bit `N` is set when + /// metamorphic opcode `N` is reachable in the account's code, and bit + /// `0xEF` alone is set when that code's first byte is the reserved + /// `0xEF`. Zero when neither holds. + function scanMetamorphicRisk(address account) external view returns (uint256); + + /// @notice Removes the last 53 bytes of `bytecode` when they are the exact + /// Solidity CBOR metadata structure the trimmer recognises. Metadata in any + /// other shape is not trimmed and does not revert. Reverts + /// `EOFBytecodeNotSupported` when `bytecode` is EOF. + /// @dev See `LibExtrospectBytecode.tryTrimSolidityCBORMetadata`, which + /// takes `bytes memory` and trims it in place. Across this external + /// interface the argument arrives as a fresh copy decoded from calldata + /// into the callee's memory, so the caller's own `bytecode` is untouched: + /// the trim lands on that copy and the copy comes back as the second + /// return value. + /// @param bytecode The bytecode to trim. + /// @return True when the last 53 bytes matched and were removed. + /// @return `bytecode` less its last 53 bytes when the first return is + /// true, otherwise `bytecode` unchanged. + function tryTrimSolidityCBORMetadata(bytes memory bytecode) external pure returns (bool, bytes memory); +} diff --git a/test/concrete/ExtrospectV2Fixture.sol b/test/concrete/ExtrospectV2Fixture.sol new file mode 100644 index 0000000..794faab --- /dev/null +++ b/test/concrete/ExtrospectV2Fixture.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {IExtrospectV2} from "src/interface/IExtrospectV2.sol"; +import {LibExtrospectBytecode} from "src/lib/LibExtrospectBytecode.sol"; +import {LibExtrospectERC1167Proxy} from "src/lib/LibExtrospectERC1167Proxy.sol"; +import {LibExtrospectERC1967BeaconProxy} from "src/lib/LibExtrospectERC1967BeaconProxy.sol"; +import {LibExtrospectMetamorphic} from "src/lib/LibExtrospectMetamorphic.sol"; + +/// @dev Test-only concrete binding `IExtrospectV2` to the libraries: every +/// function forwards to the library function it names, the shape the +/// deployable concrete in rain.extrospection.deploy takes. Exists so this +/// repo's tests exercise the declared interface surface against the +/// libraries; it is not the deployable concrete and pins no deploy +/// address. +contract ExtrospectV2Fixture is IExtrospectV2 { + /// @inheritdoc IExtrospectV2 + function checkCBORTrimmedBytecodeHash(address account, bytes32 expectedTrimmedHash) external view { + LibExtrospectBytecode.checkCBORTrimmedBytecodeHash(account, expectedTrimmedHash); + } + + /// @inheritdoc IExtrospectV2 + function checkNoSolidityCBORMetadata(address account) external view { + LibExtrospectBytecode.checkNoSolidityCBORMetadata(account); + } + + /// @inheritdoc IExtrospectV2 + function checkNotEOFBytecode(bytes memory bytecode) external pure { + LibExtrospectBytecode.checkNotEOFBytecode(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function checkNotMetamorphic(bytes memory bytecode) external pure { + LibExtrospectMetamorphic.checkNotMetamorphic(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function checkNotMetamorphic(address account) external view { + LibExtrospectMetamorphic.checkNotMetamorphic(account.code); + } + + /// @inheritdoc IExtrospectV2 + function isBeaconImplementationBytecode(address beacon, bytes32 expectedRuntimeHash) external view returns (bool) { + return LibExtrospectERC1967BeaconProxy.isBeaconImplementationBytecode(beacon, expectedRuntimeHash); + } + + /// @inheritdoc IExtrospectV2 + function isBeaconOwner(address beacon, address expectedOwner) external view returns (bool) { + return LibExtrospectERC1967BeaconProxy.isBeaconOwner(beacon, expectedOwner); + } + + /// @inheritdoc IExtrospectV2 + function isEOFBytecode(bytes memory bytecode) external pure returns (bool) { + return LibExtrospectBytecode.isEOFBytecode(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function isERC1167Proxy(bytes memory bytecode) external pure returns (bool, address) { + // False positive: tuple pass-through — both components re-emitted as + // this function's own return, nothing discarded. + // slither-disable-next-line unused-return + return LibExtrospectERC1167Proxy.isERC1167Proxy(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function scanEVMOpcodesPresentInBytecode(bytes memory bytecode) external pure returns (uint256) { + return LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function scanEVMOpcodesPresentInBytecode(address account) external view returns (uint256) { + return LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode(account.code); + } + + /// @inheritdoc IExtrospectV2 + function scanEVMOpcodesReachableInBytecode(bytes memory bytecode) external pure returns (uint256) { + return LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function scanEVMOpcodesReachableInBytecode(address account) external view returns (uint256) { + return LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(account.code); + } + + /// @inheritdoc IExtrospectV2 + function scanMetamorphicRisk(bytes memory bytecode) external pure returns (uint256) { + return LibExtrospectMetamorphic.scanMetamorphicRisk(bytecode); + } + + /// @inheritdoc IExtrospectV2 + function scanMetamorphicRisk(address account) external view returns (uint256) { + return LibExtrospectMetamorphic.scanMetamorphicRisk(account.code); + } + + /// @inheritdoc IExtrospectV2 + function tryTrimSolidityCBORMetadata(bytes memory bytecode) external pure returns (bool, bytes memory) { + bool didTrim = LibExtrospectBytecode.tryTrimSolidityCBORMetadata(bytecode); + return (didTrim, bytecode); + } +} diff --git a/test/src/interface/IExtrospectV2.t.sol b/test/src/interface/IExtrospectV2.t.sol new file mode 100644 index 0000000..d9168d2 --- /dev/null +++ b/test/src/interface/IExtrospectV2.t.sol @@ -0,0 +1,367 @@ +// 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 {IExtrospectV1} from "src/interface/IExtrospectV1.sol"; +import {IExtrospectV2} from "src/interface/IExtrospectV2.sol"; +import {LibExtrospectBytecode} from "src/lib/LibExtrospectBytecode.sol"; +import {LibExtrospectMetamorphic, EIP3541_RESERVED_LEAD_BYTE} from "src/lib/LibExtrospectMetamorphic.sol"; +import {EVM_OP_STOP, EVM_OP_ADD, EVM_OP_SELFDESTRUCT} from "src/lib/EVMOpcodes.sol"; + +import {ExtrospectV2Fixture} from "test/concrete/ExtrospectV2Fixture.sol"; +import {NonMetamorphic} from "test/concrete/NonMetamorphic.sol"; +import {HasSelfdestruct} from "test/concrete/HasSelfdestruct.sol"; +import {MockBeacon} from "test/concrete/MockBeacon.sol"; +import { + SOLIDITY_CBOR_RUNTIME_FIXTURE, + SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED +} from "test/concrete/SolidityCBORFixture.sol"; +import {LibExtrospectTestEtch} from "test/lib/LibExtrospectTestEtch.sol"; +import {LibEIP7702Designator} from "test/lib/LibEIP7702Designator.sol"; + +/// @title IExtrospectV2Test +/// @notice Binds the `IExtrospectV2` surface to the libraries through +/// `ExtrospectV2Fixture`, a concrete that forwards every interface function +/// to the library function it names — the shape the deployable concrete in +/// rain.extrospection.deploy takes. The carried V1 surface is pinned as +/// selector-identical and behaviourally live; the address-taking verdict +/// entry points added in V2 are pinned to revert `CodelessAccount` on an +/// account with no code and to agree with the bytes entry points over an +/// account's code otherwise. +contract IExtrospectV2Test is Test { + IExtrospectV2 internal immutable iExtrospect; + + constructor() { + iExtrospect = IExtrospectV2(address(new ExtrospectV2Fixture())); + } + + /// Every function carried from `IExtrospectV1` keeps its V1 selector, so + /// per-function tooling written against the V1 ABI reads the new deploy + /// unchanged. Overloaded names have no unambiguous `.selector` member on + /// V2; their bytes overloads are exercised by V1 selector below in + /// `testIExtrospectV2AnswersIExtrospectV1OverloadedSelectors`. + function testIExtrospectV2CarriesIExtrospectV1Selectors() external pure { + assertEq( + IExtrospectV2.checkCBORTrimmedBytecodeHash.selector, IExtrospectV1.checkCBORTrimmedBytecodeHash.selector + ); + assertEq( + IExtrospectV2.checkNoSolidityCBORMetadata.selector, IExtrospectV1.checkNoSolidityCBORMetadata.selector + ); + assertEq(IExtrospectV2.checkNotEOFBytecode.selector, IExtrospectV1.checkNotEOFBytecode.selector); + assertEq( + IExtrospectV2.isBeaconImplementationBytecode.selector, + IExtrospectV1.isBeaconImplementationBytecode.selector + ); + assertEq(IExtrospectV2.isBeaconOwner.selector, IExtrospectV1.isBeaconOwner.selector); + assertEq(IExtrospectV2.isEOFBytecode.selector, IExtrospectV1.isEOFBytecode.selector); + assertEq(IExtrospectV2.isERC1167Proxy.selector, IExtrospectV1.isERC1167Proxy.selector); + assertEq( + IExtrospectV2.tryTrimSolidityCBORMetadata.selector, IExtrospectV1.tryTrimSolidityCBORMetadata.selector + ); + } + + /// The four names V2 overloads answer their V1 (bytes-taking) selectors + /// with the library's verdicts: raw calls encoded from the V1 ABI land on + /// the V2 fixture and return what the libraries return. + function testIExtrospectV2AnswersIExtrospectV1OverloadedSelectors() external view { + (bool ok, bytes memory ret) = address(iExtrospect).staticcall( + abi.encodeWithSelector(IExtrospectV1.scanMetamorphicRisk.selector, bytes(hex"ff")) + ); + assertTrue(ok); + assertEq(abi.decode(ret, (uint256)), 1 << EVM_OP_SELFDESTRUCT); + + (ok, ret) = address(iExtrospect).staticcall( + abi.encodeWithSelector(IExtrospectV1.checkNotMetamorphic.selector, bytes(hex"0001")) + ); + assertTrue(ok); + assertEq(ret.length, 0); + + (ok, ret) = address(iExtrospect).staticcall( + abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesPresentInBytecode.selector, bytes(hex"0001")) + ); + assertTrue(ok); + assertEq(abi.decode(ret, (uint256)), (1 << EVM_OP_STOP) | (1 << EVM_OP_ADD)); + + (ok, ret) = address(iExtrospect).staticcall( + abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesReachableInBytecode.selector, bytes(hex"0001")) + ); + assertTrue(ok); + assertEq(abi.decode(ret, (uint256)), 1 << EVM_OP_STOP); + } + + /// Carried surface: `checkCBORTrimmedBytecodeHash` passes on the trimmed + /// hash of code carrying the standard trailer, reverts + /// `BytecodeHashMismatch` on any other hash, and reverts + /// `MetadataNotTrimmed` for an account compiled without metadata. + function testIExtrospectV2CheckCBORTrimmedBytecodeHash() external { + address target = address(0xBEEF); + vm.etch(target, SOLIDITY_CBOR_RUNTIME_FIXTURE); + iExtrospect.checkCBORTrimmedBytecodeHash(target, keccak256(SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED)); + + bytes32 wrong = keccak256("wrong"); + vm.expectRevert( + abi.encodeWithSelector( + LibExtrospectBytecode.BytecodeHashMismatch.selector, + wrong, + keccak256(SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED) + ) + ); + iExtrospect.checkCBORTrimmedBytecodeHash(target, wrong); + + NonMetamorphic noMetadata = new NonMetamorphic(); + vm.expectRevert(LibExtrospectBytecode.MetadataNotTrimmed.selector); + iExtrospect.checkCBORTrimmedBytecodeHash(address(noMetadata), keccak256(address(noMetadata).code)); + } + + /// Carried surface: `checkNoSolidityCBORMetadata` passes on an account + /// compiled without metadata, reverts `UnexpectedMetadata` on the standard + /// trailer, and reverts `CodelessAccount` on an account with no code. + function testIExtrospectV2CheckNoSolidityCBORMetadata() external { + NonMetamorphic clean = new NonMetamorphic(); + iExtrospect.checkNoSolidityCBORMetadata(address(clean)); + + address target = address(0xBEEF); + vm.etch(target, SOLIDITY_CBOR_RUNTIME_FIXTURE); + vm.expectRevert(LibExtrospectBytecode.UnexpectedMetadata.selector); + iExtrospect.checkNoSolidityCBORMetadata(target); + + address codeless = address(0xC2); + assertEq(codeless.code.length, 0); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectBytecode.CodelessAccount.selector, codeless)); + iExtrospect.checkNoSolidityCBORMetadata(codeless); + } + + /// Carried surface: `checkNotEOFBytecode` and `isEOFBytecode` gate on the + /// two byte EOF magic `0xEF00` only. A bare `0xEF` and the `0xEF01` of an + /// EIP-7702 delegation designator are not EOF. + function testIExtrospectV2EOFBytecode() external { + assertTrue(iExtrospect.isEOFBytecode(hex"ef00")); + assertTrue(iExtrospect.isEOFBytecode(hex"ef0001")); + assertFalse(iExtrospect.isEOFBytecode(hex"")); + assertFalse(iExtrospect.isEOFBytecode(hex"ef")); + assertFalse(iExtrospect.isEOFBytecode(LibEIP7702Designator.designator(address(0xDE1E647E)))); + + iExtrospect.checkNotEOFBytecode(hex""); + iExtrospect.checkNotEOFBytecode(hex"ef"); + iExtrospect.checkNotEOFBytecode(LibEIP7702Designator.designator(address(0xDE1E647E))); + vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); + iExtrospect.checkNotEOFBytecode(hex"ef00"); + } + + /// Carried surface: `checkNotMetamorphic(bytes)` stays total over bytes. + /// Empty bytecode passes — whether an account has code at all is not + /// checked at the bytes boundary — clean code passes, reachable + /// metamorphic opcodes revert `Metamorphic` with the scan's bitmap, and + /// any `0xEF` lead byte fails closed to `Metamorphic(1 << 0xEF)`. + function testIExtrospectV2CheckNotMetamorphicBytes() external { + iExtrospect.checkNotMetamorphic(hex""); + + NonMetamorphic clean = new NonMetamorphic(); + iExtrospect.checkNotMetamorphic(address(clean).code); + + HasSelfdestruct risky = new HasSelfdestruct(); + uint256 risk = iExtrospect.scanMetamorphicRisk(address(risky).code); + assertTrue(risk != 0); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, risk)); + iExtrospect.checkNotMetamorphic(address(risky).code); + + vm.expectRevert( + abi.encodeWithSelector( + LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << EIP3541_RESERVED_LEAD_BYTE + ) + ); + iExtrospect.checkNotMetamorphic(hex"ef"); + } + + /// Carried surface: `isERC1167Proxy` recognises the 45 byte minimal proxy + /// and extracts its implementation address. + function testIExtrospectV2IsERC1167Proxy() external view { + address implementation = address(0x1234567890123456789012345678901234567890); + bytes memory proxy = abi.encodePacked( + hex"363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" + ); + (bool result, address extracted) = iExtrospect.isERC1167Proxy(proxy); + assertTrue(result); + assertEq(extracted, implementation); + + (result, extracted) = iExtrospect.isERC1167Proxy(hex"00"); + assertFalse(result); + assertEq(extracted, address(0)); + } + + /// Carried surface: the beacon predicates answer over a mock beacon. + function testIExtrospectV2BeaconPredicates() external { + NonMetamorphic implementation = new NonMetamorphic(); + address owner = address(0x0111); + MockBeacon beacon = new MockBeacon(address(implementation), owner); + + assertTrue(iExtrospect.isBeaconImplementationBytecode(address(beacon), keccak256(address(implementation).code))); + assertFalse(iExtrospect.isBeaconImplementationBytecode(address(beacon), keccak256("wrong"))); + + assertTrue(iExtrospect.isBeaconOwner(address(beacon), owner)); + assertFalse(iExtrospect.isBeaconOwner(address(beacon), address(0x0222))); + } + + /// Carried surface: `tryTrimSolidityCBORMetadata` trims the standard + /// trailer on the callee's copy and returns it; the caller's own bytes + /// are untouched across the external boundary. Unrecognised bytes come + /// back unchanged with a false verdict. + function testIExtrospectV2TryTrimSolidityCBORMetadata() external view { + bytes memory bytecode = SOLIDITY_CBOR_RUNTIME_FIXTURE; + (bool didTrim, bytes memory trimmed) = iExtrospect.tryTrimSolidityCBORMetadata(bytecode); + assertTrue(didTrim); + assertEq(trimmed, SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED); + assertEq(bytecode.length, SOLIDITY_CBOR_RUNTIME_FIXTURE.length); + + (bool didTrimAgain, bytes memory untrimmed) = + iExtrospect.tryTrimSolidityCBORMetadata(SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED); + assertFalse(didTrimAgain); + assertEq(untrimmed, SOLIDITY_CBOR_RUNTIME_FIXTURE_TRIMMED); + } + + /// V2 surface: `checkNotMetamorphic(address)` reverts `CodelessAccount` + /// carrying the address when the account has no code. The bytes entry + /// point passes the same account's empty code, pinning that only the + /// address boundary refuses to vouch for a codeless account. + function testIExtrospectV2CheckNotMetamorphicAddressRevertsOnCodelessAccount() external { + address codeless = address(0xC2); + assertEq(codeless.code.length, 0); + iExtrospect.checkNotMetamorphic(codeless.code); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectBytecode.CodelessAccount.selector, codeless)); + iExtrospect.checkNotMetamorphic(codeless); + } + + /// V2 surface: `scanMetamorphicRisk(address)` reverts `CodelessAccount` + /// carrying the address when the account has no code. The bytes entry + /// point scans the same account's empty code to zero. + function testIExtrospectV2ScanMetamorphicRiskAddressRevertsOnCodelessAccount() external { + address codeless = address(0xC2); + assertEq(codeless.code.length, 0); + assertEq(iExtrospect.scanMetamorphicRisk(codeless.code), 0); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectBytecode.CodelessAccount.selector, codeless)); + iExtrospect.scanMetamorphicRisk(codeless); + } + + /// V2 surface: `scanEVMOpcodesPresentInBytecode(address)` reverts + /// `CodelessAccount` carrying the address when the account has no code. + /// The bytes entry point scans the same account's empty code to zero. + function testIExtrospectV2ScanPresentAddressRevertsOnCodelessAccount() external { + address codeless = address(0xC2); + assertEq(codeless.code.length, 0); + assertEq(iExtrospect.scanEVMOpcodesPresentInBytecode(codeless.code), 0); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectBytecode.CodelessAccount.selector, codeless)); + iExtrospect.scanEVMOpcodesPresentInBytecode(codeless); + } + + /// V2 surface: `scanEVMOpcodesReachableInBytecode(address)` reverts + /// `CodelessAccount` carrying the address when the account has no code. + /// The bytes entry point scans the same account's empty code to zero. + function testIExtrospectV2ScanReachableAddressRevertsOnCodelessAccount() external { + address codeless = address(0xC2); + assertEq(codeless.code.length, 0); + assertEq(iExtrospect.scanEVMOpcodesReachableInBytecode(codeless.code), 0); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectBytecode.CodelessAccount.selector, codeless)); + iExtrospect.scanEVMOpcodesReachableInBytecode(codeless); + } + + /// V2 surface: for an account with code the metamorphic pair binds the + /// bytes verdicts to the account: clean passes, reachable metamorphic + /// opcodes revert `Metamorphic` with the same bitmap the bytes scan + /// reports. + function testIExtrospectV2MetamorphicAddressAgreesWithBytes() external { + NonMetamorphic clean = new NonMetamorphic(); + iExtrospect.checkNotMetamorphic(address(clean)); + assertEq(iExtrospect.scanMetamorphicRisk(address(clean)), 0); + + HasSelfdestruct risky = new HasSelfdestruct(); + uint256 risk = iExtrospect.scanMetamorphicRisk(address(risky).code); + assertTrue(risk != 0); + assertEq(iExtrospect.scanMetamorphicRisk(address(risky)), risk); + vm.expectRevert(abi.encodeWithSelector(LibExtrospectMetamorphic.Metamorphic.selector, risk)); + iExtrospect.checkNotMetamorphic(address(risky)); + } + + /// V2 surface: account code carrying an EIP-7702 delegation designator + /// (`0xEF0100 || address`) fails closed on the metamorphic pair to + /// exactly `1 << 0xEF`, never `EOFBytecodeNotSupported`. + function testIExtrospectV2MetamorphicAddressFailsClosedOnEIP7702Designator() external { + address target = address(0xBEEF); + vm.etch(target, LibEIP7702Designator.designator(address(0xDE1E647E))); + assertEq(iExtrospect.scanMetamorphicRisk(target), uint256(1) << EIP3541_RESERVED_LEAD_BYTE); + vm.expectRevert( + abi.encodeWithSelector( + LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << EIP3541_RESERVED_LEAD_BYTE + ) + ); + iExtrospect.checkNotMetamorphic(target); + } + + /// V2 surface: the same account code splits the two verdict families. + /// EOF code (`0xEF00` lead) reverts `EOFBytecodeNotSupported` on the raw + /// opcode scans, which gate on the two byte EOF magic only, while the + /// metamorphic pair fails closed to `Metamorphic(1 << 0xEF)` on the lead + /// byte alone and never reverts `EOFBytecodeNotSupported`. + function testIExtrospectV2AddressEntryPointsSplitOnEOFCode() external { + address target = address(0xBEEF); + vm.etch(target, hex"ef00"); + + vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); + iExtrospect.scanEVMOpcodesPresentInBytecode(target); + vm.expectRevert(LibExtrospectBytecode.EOFBytecodeNotSupported.selector); + iExtrospect.scanEVMOpcodesReachableInBytecode(target); + + assertEq(iExtrospect.scanMetamorphicRisk(target), uint256(1) << EIP3541_RESERVED_LEAD_BYTE); + vm.expectRevert( + abi.encodeWithSelector( + LibExtrospectMetamorphic.Metamorphic.selector, uint256(1) << EIP3541_RESERVED_LEAD_BYTE + ) + ); + iExtrospect.checkNotMetamorphic(target); + } + + /// V2 surface: a bare `0xEF` lead byte that is not the EOF magic does not + /// trip the raw scans' EOF gate — the byte scans as an ordinary unassigned + /// opcode — while the metamorphic pair still fails closed on it. + function testIExtrospectV2AddressEntryPointsSplitOnBare0xEF() external { + address target = address(0xBEEF); + vm.etch(target, hex"ef"); + + assertEq(iExtrospect.scanEVMOpcodesPresentInBytecode(target), uint256(1) << EIP3541_RESERVED_LEAD_BYTE); + assertEq(iExtrospect.scanEVMOpcodesReachableInBytecode(target), uint256(1) << EIP3541_RESERVED_LEAD_BYTE); + assertEq(iExtrospect.scanMetamorphicRisk(target), uint256(1) << EIP3541_RESERVED_LEAD_BYTE); + } + + /// V2 surface: for an account with code the opcode scans report the same + /// bitmaps as the bytes scans over that account's code, and present and + /// reachable stay distinct scans: code with a region after a halt and + /// before any `JUMPDEST` sets present bits that reachable omits. + function testIExtrospectV2OpcodeScanAddressAgreesWithBytes() external { + address target = address(0xBEEF); + vm.etch(target, SOLIDITY_CBOR_RUNTIME_FIXTURE); + + uint256 present = iExtrospect.scanEVMOpcodesPresentInBytecode(SOLIDITY_CBOR_RUNTIME_FIXTURE); + uint256 reachable = iExtrospect.scanEVMOpcodesReachableInBytecode(SOLIDITY_CBOR_RUNTIME_FIXTURE); + assertTrue(present != reachable); + + assertEq(iExtrospect.scanEVMOpcodesPresentInBytecode(target), present); + assertEq(iExtrospect.scanEVMOpcodesReachableInBytecode(target), reachable); + } + + /// Fuzz: for an account with nonempty etchable non-EOF code, every V2 + /// address entry point agrees with its bytes entry point over the + /// account's code. + function testIExtrospectV2AddressEntryPointsAgreeWithBytesFuzz(bytes memory code) external { + vm.assume(code.length > 0); + vm.assume(!iExtrospect.isEOFBytecode(code)); + address target = address(0xBEEF); + LibExtrospectTestEtch.assumeEtch(vm, target, code); + + assertEq(iExtrospect.scanEVMOpcodesPresentInBytecode(target), iExtrospect.scanEVMOpcodesPresentInBytecode(code)); + assertEq( + iExtrospect.scanEVMOpcodesReachableInBytecode(target), iExtrospect.scanEVMOpcodesReachableInBytecode(code) + ); + assertEq(iExtrospect.scanMetamorphicRisk(target), iExtrospect.scanMetamorphicRisk(code)); + } +} From 420b59b9c11c7d97063f856545c71f639197099f Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 19 Aug 2026 14:22:47 +0000 Subject: [PATCH 2/3] Bind IExtrospectV2 address entry points to the address-taking libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #139: ExtrospectV2Fixture now forwards checkNotMetamorphic(address), scanMetamorphicRisk(address), scanEVMOpcodesPresentInBytecode(address) and scanEVMOpcodesReachableInBytecode(address) to the library functions of the same name and argument — the address-taking entry points from PRs #136 and #141 — instead of routing through the bytes functions over account.code. The four discriminating codeless-account tests from the previous commit go green: only the address boundary has the information to refuse to vouch for an account with no code, and the fixture now carries that boundary the same way the deployable concrete in rain.extrospection.deploy will. README interface section now tells the two-interface story: IExtrospectV1 frozen with its deployed concrete, IExtrospectV2 carrying the whole V1 surface unchanged plus the address-taking verdict entry points, for a new deploy. The orphaned-bitmaps note covers both interfaces. nix develop -c forge test: 417 passed, 3 failed — only the fork tests needing ARBITRUM_RPC_URL (environmental, fail on main too). With --no-match-path "*fork*": 417 passed, 0 failed. Co-Authored-By: Claude Fable 5 --- README.md | 27 +++++++++++++++++---------- test/concrete/ExtrospectV2Fixture.sol | 8 ++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 079f265..6b61801 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,20 @@ Efforts have been made to implement the logic efficiently but it is expected that the primary execution environment will be offchain, so there are somewhat gas intensive algorithms in this repository. -### `IExtrospectV1` - -`src/interface/IExtrospectV1.sol` is the external interface. Each of its -functions forwards to the library function of the same name and does nothing -else, so the interface is a call-and-return view of the libraries below. The -concrete implementation (`Extrospect`) and its deterministic deployment live in -the rain.extrospection.deploy repo. +### `IExtrospectV1` and `IExtrospectV2` + +`src/interface/IExtrospectV1.sol` and `src/interface/IExtrospectV2.sol` are the +external interfaces. Each of their functions forwards to the library function of +the same name and does nothing else, so each interface is a call-and-return view +of the libraries below. `IExtrospectV1` is frozen in practice: its concrete +implementation (`Extrospect`) is deterministically deployed and can never gain +functions. `IExtrospectV2`, for a new deploy, carries the whole V1 surface +unchanged and adds the address-taking verdict entry points that since landed at +the library level — `checkNotMetamorphic(address)`, +`scanMetamorphicRisk(address)`, `scanEVMOpcodesPresentInBytecode(address)` and +`scanEVMOpcodesReachableInBytecode(address)` — every one reverting +`CodelessAccount` on an account with no code. The concretes, their deploy +records and their releases live in the rain.extrospection.deploy repo. `src/interface/IBeacon.sol` and `src/interface/IOwnable.sol` are the minimal `implementation()` and `owner()` interfaces used to query beacons. @@ -179,9 +186,9 @@ The derived bitmaps are: `DELEGATECALL` and `CALLCODE`. `NON_STATIC_OPS` and `INTERPRETER_DISALLOWED_OPS` are exported constants only. -No library and no `IExtrospectV1` function in this repository reads either of -them. A caller wanting an interpreter safety check masks a reachable scan -against `INTERPRETER_DISALLOWED_OPS` itself. +No library and no `IExtrospectV1` or `IExtrospectV2` function in this repository +reads either of them. A caller wanting an interpreter safety check masks a +reachable scan against `INTERPRETER_DISALLOWED_OPS` itself. The opcode constants, and the bitmaps derived from them, are subject to change if/when new opcodes are supported by the EVM due to future hard forks. diff --git a/test/concrete/ExtrospectV2Fixture.sol b/test/concrete/ExtrospectV2Fixture.sol index 794faab..ffe71bc 100644 --- a/test/concrete/ExtrospectV2Fixture.sol +++ b/test/concrete/ExtrospectV2Fixture.sol @@ -37,7 +37,7 @@ contract ExtrospectV2Fixture is IExtrospectV2 { /// @inheritdoc IExtrospectV2 function checkNotMetamorphic(address account) external view { - LibExtrospectMetamorphic.checkNotMetamorphic(account.code); + LibExtrospectMetamorphic.checkNotMetamorphic(account); } /// @inheritdoc IExtrospectV2 @@ -70,7 +70,7 @@ contract ExtrospectV2Fixture is IExtrospectV2 { /// @inheritdoc IExtrospectV2 function scanEVMOpcodesPresentInBytecode(address account) external view returns (uint256) { - return LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode(account.code); + return LibExtrospectBytecode.scanEVMOpcodesPresentInBytecode(account); } /// @inheritdoc IExtrospectV2 @@ -80,7 +80,7 @@ contract ExtrospectV2Fixture is IExtrospectV2 { /// @inheritdoc IExtrospectV2 function scanEVMOpcodesReachableInBytecode(address account) external view returns (uint256) { - return LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(account.code); + return LibExtrospectBytecode.scanEVMOpcodesReachableInBytecode(account); } /// @inheritdoc IExtrospectV2 @@ -90,7 +90,7 @@ contract ExtrospectV2Fixture is IExtrospectV2 { /// @inheritdoc IExtrospectV2 function scanMetamorphicRisk(address account) external view returns (uint256) { - return LibExtrospectMetamorphic.scanMetamorphicRisk(account.code); + return LibExtrospectMetamorphic.scanMetamorphicRisk(account); } /// @inheritdoc IExtrospectV2 From 25942df465509af82104bfb860ff620788905be2 Mon Sep 17 00:00:00 2001 From: baku-ccron Date: Wed, 19 Aug 2026 14:30:36 +0000 Subject: [PATCH 3/3] forge fmt the V2 selector test CI's forge fmt collapses the short assertEq calls this file's local format kept multiline; formatted with the pinned rainix toolchain so the static gate passes. Co-Authored-By: Claude Fable 5 --- test/src/interface/IExtrospectV2.t.sol | 40 +++++++++++--------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/test/src/interface/IExtrospectV2.t.sol b/test/src/interface/IExtrospectV2.t.sol index d9168d2..d2710a9 100644 --- a/test/src/interface/IExtrospectV2.t.sol +++ b/test/src/interface/IExtrospectV2.t.sol @@ -46,47 +46,42 @@ contract IExtrospectV2Test is Test { assertEq( IExtrospectV2.checkCBORTrimmedBytecodeHash.selector, IExtrospectV1.checkCBORTrimmedBytecodeHash.selector ); - assertEq( - IExtrospectV2.checkNoSolidityCBORMetadata.selector, IExtrospectV1.checkNoSolidityCBORMetadata.selector - ); + assertEq(IExtrospectV2.checkNoSolidityCBORMetadata.selector, IExtrospectV1.checkNoSolidityCBORMetadata.selector); assertEq(IExtrospectV2.checkNotEOFBytecode.selector, IExtrospectV1.checkNotEOFBytecode.selector); assertEq( - IExtrospectV2.isBeaconImplementationBytecode.selector, - IExtrospectV1.isBeaconImplementationBytecode.selector + IExtrospectV2.isBeaconImplementationBytecode.selector, IExtrospectV1.isBeaconImplementationBytecode.selector ); assertEq(IExtrospectV2.isBeaconOwner.selector, IExtrospectV1.isBeaconOwner.selector); assertEq(IExtrospectV2.isEOFBytecode.selector, IExtrospectV1.isEOFBytecode.selector); assertEq(IExtrospectV2.isERC1167Proxy.selector, IExtrospectV1.isERC1167Proxy.selector); - assertEq( - IExtrospectV2.tryTrimSolidityCBORMetadata.selector, IExtrospectV1.tryTrimSolidityCBORMetadata.selector - ); + assertEq(IExtrospectV2.tryTrimSolidityCBORMetadata.selector, IExtrospectV1.tryTrimSolidityCBORMetadata.selector); } /// The four names V2 overloads answer their V1 (bytes-taking) selectors /// with the library's verdicts: raw calls encoded from the V1 ABI land on /// the V2 fixture and return what the libraries return. function testIExtrospectV2AnswersIExtrospectV1OverloadedSelectors() external view { - (bool ok, bytes memory ret) = address(iExtrospect).staticcall( - abi.encodeWithSelector(IExtrospectV1.scanMetamorphicRisk.selector, bytes(hex"ff")) - ); + (bool ok, bytes memory ret) = address(iExtrospect) + .staticcall(abi.encodeWithSelector(IExtrospectV1.scanMetamorphicRisk.selector, bytes(hex"ff"))); assertTrue(ok); assertEq(abi.decode(ret, (uint256)), 1 << EVM_OP_SELFDESTRUCT); - (ok, ret) = address(iExtrospect).staticcall( - abi.encodeWithSelector(IExtrospectV1.checkNotMetamorphic.selector, bytes(hex"0001")) - ); + (ok, ret) = address(iExtrospect) + .staticcall(abi.encodeWithSelector(IExtrospectV1.checkNotMetamorphic.selector, bytes(hex"0001"))); assertTrue(ok); assertEq(ret.length, 0); - (ok, ret) = address(iExtrospect).staticcall( - abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesPresentInBytecode.selector, bytes(hex"0001")) - ); + (ok, ret) = address(iExtrospect) + .staticcall( + abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesPresentInBytecode.selector, bytes(hex"0001")) + ); assertTrue(ok); assertEq(abi.decode(ret, (uint256)), (1 << EVM_OP_STOP) | (1 << EVM_OP_ADD)); - (ok, ret) = address(iExtrospect).staticcall( - abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesReachableInBytecode.selector, bytes(hex"0001")) - ); + (ok, ret) = address(iExtrospect) + .staticcall( + abi.encodeWithSelector(IExtrospectV1.scanEVMOpcodesReachableInBytecode.selector, bytes(hex"0001")) + ); assertTrue(ok); assertEq(abi.decode(ret, (uint256)), 1 << EVM_OP_STOP); } @@ -179,9 +174,8 @@ contract IExtrospectV2Test is Test { /// and extracts its implementation address. function testIExtrospectV2IsERC1167Proxy() external view { address implementation = address(0x1234567890123456789012345678901234567890); - bytes memory proxy = abi.encodePacked( - hex"363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3" - ); + bytes memory proxy = + abi.encodePacked(hex"363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3"); (bool result, address extracted) = iExtrospect.isERC1167Proxy(proxy); assertTrue(result); assertEq(extracted, implementation);