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. |
APPROVE (0xaa)
Section titled “APPROVE (0xaa)”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.
TXPARAM (0xb0)
Section titled “TXPARAM (0xb0)”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.
FRAMEDATALOAD (0xb1)
Section titled “FRAMEDATALOAD (0xb1)”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.
FRAMEDATACOPY (0xb2)
Section titled “FRAMEDATACOPY (0xb2)”Stack: [memOffset, dataOffset, length, frameIndex] → []
Copies length bytes of a frame’s data into memory at memOffset.
CALLDATACOPY semantics and gas, including memory expansion.
FRAMEPARAM (0xb3)
Section titled “FRAMEPARAM (0xb3)”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.
SIGPARAM (0xb4)
Section titled “SIGPARAM (0xb4)”Stack (value selectors 0x00–0x03): [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.
How an account uses these
Section titled “How an account uses these”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: revertTwo 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.
Validation restrictions
Section titled “Validation restrictions”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.