Skip to content

Frame opcodes

EIP-8141 adds six opcodes. They let contract code inspect the frame transaction it is executing inside, and — crucially — read raw signature bytes that the protocol itself does not interpret.

All six are only valid inside a frame-transaction context. Executing them outside one is an invalid opcode.

Opcode Byte Purpose
APPROVE 0xaa Authorise payment and/or execution.
TXPARAM 0xb0 Read a transaction-level parameter.
FRAMEDATALOAD 0xb1 Read 32 bytes of a frame’s calldata.
FRAMEDATACOPY 0xb2 Copy a frame’s calldata into memory.
FRAMEPARAM 0xb3 Read a per-frame parameter.
SIGPARAM 0xb4 Read a signature entry, including raw bytes.

Authorises the transaction to proceed. A VERIFY frame that does not call APPROVE blocks the transaction.

The scope a frame may approve is bounded by its flags: bit 0 permits approving payment, bit 1 permits approving execution. A frame cannot approve a scope its flags do not allow, regardless of what its code does.

This is the entire authorisation mechanism. A post-quantum account’s VERIFY frame verifies a signature and calls APPROVE only if the verifier precompile returned 1.

Stack: [selector] → [value]

Reads a transaction-level parameter.

Selector Value
0x00 Transaction type (0x06)
0x01 Nonce
0x02 Sender address
0x03 tip_cap
0x04 fee_cap
0x05 blob_fee_cap
0x06 Maximum cost
0x07 Number of blob hashes
0x08 Sig-hash — the 32-byte hash the witness signs
0x09 Frame count
0x0a Index of the currently executing frame
0x0b Signature entry count

Selector 0x08 is the important one for account code: it is the message a post-quantum witness signs, so it is what gets passed to the verifier precompile.

Any other selector is an invalid opcode.

Stack: [offset, frameIndex] → [value]

Reads 32 bytes of a frame’s data at offset, zero-padded past the end — CALLDATALOAD semantics, for an arbitrary frame rather than the current call.

Stack: [memOffset, dataOffset, length, frameIndex] → []

Copies length bytes of a frame’s data into memory at memOffset. CALLDATACOPY semantics and gas, including memory expansion.

Stack: [frameIndex, selector] → [value]

Reads a parameter of the frame at frameIndex.

Selector Value
0x00 Target address (resolved — an empty target reads as tx.sender)
0x01 Gas limit
0x02 Mode
0x03 Flags
0x04 Length of data
0x05 Status — 1 if that frame succeeded
0x06 Allowed approval scope (the approve bits of flags)
0x07 Atomic-batch flag
0x08 Value

Selector 0x05 can only read frames that have already executed. Querying the status of the current or a later frame is an invalid opcode, which keeps frames from depending on results that do not exist yet.

Stack (value selectors 0x000x03): [signatureIndex, selector] → [value] Stack (copy selector 0x04): [signatureIndex, selector, length, dataOffset, memOffset] → []

Reads a signature entry. This is the opcode that makes post-quantum accounts possible.

Selector Value
0x00 Resolved signer address — invalid for ARBITRARY entries, which have no resolved signer
0x01 Scheme
0x02 msg
0x03 Signature length
0x04 Copy raw signature bytes into memory — ARBITRARY entries only

Selector 0x04 has CALLDATACOPY semantics and gas: it copies length bytes of the entry’s signature, starting at dataOffset, into memory at memOffset, zero-padding past the end.

It is restricted to ARBITRARY entries by design. Protocol-verified schemes have already been checked before execution and there is no reason for contract code to re-read their bytes; ARBITRARY entries are exactly the ones the protocol deliberately did not interpret, so the account must read them itself.

An out-of-range signatureIndex is an invalid opcode.

A post-quantum account’s VERIFY frame is a short program built from three of these opcodes:

1. TXPARAM 0x08 → the 32-byte sig-hash
store at memory[0:32]
2. SIGPARAM 0x04 → copy the ML-DSA signature (3,309 bytes)
into memory[32 : 32+3309]
3. CODECOPY (or read the key pointer)
→ append the account's own 1,952-byte public key
4. STATICCALL 0x14 → the ML-DSA-65 verifier precompile
input: memory[0 : 5293]
output: one 32-byte word
5. if the word == 1: APPROVE
else: revert

Two properties are worth noticing.

The public key comes from the account’s own code or storage — never from the transaction. If the account accepted a caller-supplied key, an attacker would simply supply a matched key/signature pair.

The precompile always returns a 32-byte word, so there is no empty-return case for step 5 to misread as success. See Precompiled contracts.

Frame code that runs at the submission gate is additionally constrained by the ERC-7562 validation rules: a banned-opcode tracer rejects transactions whose VERIFY frame reads mutable global state, and the whole prefix must fit inside a bounded gas budget. This is an admission-time policy, not a consensus rule.

Precompiles are always callable under those rules, which is what lets a VERIFY frame invoke the ML-DSA verifier.