Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
239 changes: 239 additions & 0 deletions src/interface/IExtrospectV2.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading