---
eip: 8141
title: Frame Transaction
description: Add frame abstraction for transaction validation, execution, and gas payment
author: Vitalik Buterin (@vbuterin), lightclient (@lightclient), Felix Lange (@fjl), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Derek Chiang (@derekchiang), Toni Wahrstätter (@nerolation), Stavros Vlachakis (@svlachakis)
discussions-to: https://ethereum-magicians.org/t/frame-transaction/27617
status: Draft
type: Standards Track
category: Core
created: 2026-01-29
requires: 1559, 2718, 2780, 3529, 3607, 4844, 7594, 7623, 7702, 7708, 7778, 7825, 8037
---

## Abstract

Adds a new transaction whose validity and gas payment can be defined abstractly. This is achieved by decomposing the transaction into a sequence of *frames* which are contract calls that validate the transaction, approve gas payment, and execute standard user operations.

## Motivation

The frame transaction type offers UX and security benefits across many areas:

- it provides a native off-ramp from the elliptic curve based cryptographic system used to authenticate transactions today, to post-quantum (PQ) secure systems.
- accounts are unlinked from their ECDSA keys, allowing native key rotation
- smart accounts become simpler and therefore safer by natively providing batch call processing
- alternative fee payment schemes are supported without centralized, third party relayers
- the default account gives the protocol an even stronger guarantee on the lowest common denominator of account functionality
 
Ultimately, frame transactions realize the original vision of account abstraction: an account simply becomes an address with code. It leverages the EVM to support arbitrary *user-defined* definitions of validation and gas payment.

## Specification

### Constants

| Name                       | Value             |
|----------------------------|-------------------|
| `FRAME_TX_TYPE`            | `0x06`            |
| `FRAME_TX_INTRINSIC_COST`  | `12000`           |
| `FRAME_TX_PER_FRAME_COST`  | `475`             |
| `ENTRY_POINT`              | `address(0xaa)`   |
| `EXPIRY_VERIFIER`          | `address(0x8141)` |
| `EXPIRY_DATA_LENGTH`       | `8`               |
| `MAX_FRAMES`               | `64`              |
| `SECP256K1N`               | `0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141` |
| `SECP256R1N`               | `0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551` |

The following parameters are defined in other EIPs and referenced by this specification:

| Name | Value | Source |
|------|-------|--------|
| `TX_MAX_GAS_LIMIT` | `16,777,216` | [EIP-7825](./eip-7825.md) |
| `TX_VALUE_COST` | `6,000` | [EIP-2780](./eip-2780.md) |
| `CPSB` | `1,530` | [EIP-8037](./eip-8037.md) |
| `STATE_BYTES_PER_NEW_ACCOUNT` | `120` | [EIP-8037](./eip-8037.md) |
| `STANDARD_TOKEN_COST` | `4` | [EIP-7976](./eip-7976.md) |
| `TOTAL_COST_FLOOR_PER_TOKEN` | `16` | [EIP-7976](./eip-7976.md) |
| `MAX_REFUND_QUOTIENT` | `5` | [EIP-3529](./eip-3529.md) |
| `GAS_PER_BLOB` | `131,072` | [EIP-4844](./eip-4844.md) |
| `VERSIONED_HASH_VERSION_KZG` | `Bytes1(0x01)` | [EIP-4844](./eip-4844.md) |

### Frame Transaction

A new [EIP-2718](./eip-2718.md) transaction with type `FRAME_TX_TYPE` is introduced. Transactions of this type are referred to as **frame transactions**.

#### Payload Encoding

The payload is defined as the RLP serialization of the following:

```
[chain_id, nonce, sender, frames, signatures, fees, blob_versioned_hashes]

frames = [[mode, flags, target, limits, value, data], ...]
limits = [execution, state]
signatures = [[scheme, signer, msg, signature], ...]
fees = [max_priority_fee_per_gas, max_fee_per_gas, max_fee_per_blob_gas]
```

#### Field Definitions

Below are high-level definitions of each field in the transaction definition. Detailed behavior is defined in subsequent sections.

##### Outer transaction payload

- `chain_id` -- the chain ID in which the transaction is valid.
- `nonce` -- nonce of the sender to prevent replays.
- `sender` -- the address of the intended sender of the transaction.
- `frames` -- list of frames to execute.
- `signatures` -- list of validated signatures available to the transaction.
- `fees` -- list of fee parameters for the transaction:
  - `max_priority_fee_per_gas` -- the [EIP-1559](./eip-1559.md) priority fee per gas the transaction will pay.
  - `max_fee_per_gas` -- the maximum EIP-1559 fee the transaction is willing to pay, per gas.
  - `max_fee_per_blob_gas` -- the maximum [EIP-4844](./eip-4844.md) fee per blob gas the transaction is willing to pay. Must be `0` if `blob_versioned_hashes` is empty.
- `blob_versioned_hashes` -- list of EIP-4844 blob versioned hashes.

##### Frame object

- `mode` -- the mode specifies the specific execution semantics the frame will execute with.
- `flags` -- specifies optional frame / mode features.
- `target` -- the destination or `to` address for the frame.
- `limits` -- list of gas limits for the frame:
  - `execution` -- the maximum execution gas allowed to be expended in pursuit of the frame.
  - `state` -- the maximum state gas ([EIP-8037](./eip-8037.md)) allowed to be expended in pursuit of the frame.
- `value` -- the amount in wei that should transferred from the `sender` as part of the frame execution.
- `data` -- the calldata provided to the top level call frame.

###### Flags

The `flags` field configures additional execution constraints. Bit positions are zero-based, with the least significant bit numbered `0`.

| Flag bits | Meaning                      | Valid with  |
|-----------|------------------------------| ----------- |
| 0-1       | Approval scope               | Any mode    |
| 2         | Atomic batch                 | `DEFAULT`, `SENDER` |
| 3..       | reserved                     | No mode     |

> The `Valid with` column indicates the mode under which the flag is valid. If a flag is not valid under the current mode, the transaction is invalid.

- *Approval scope*: a bitmask with values defined in the `APPROVE` instruction section [Scope Operand](#scope-operand).
- *Atomic batch*: flag indicates that the frame is in a batch with the following frame or frames. All frames in a batch must succeed, or all of them revert. We define `ATOMIC_BATCH_FLAG` constant with value `0x4`.

##### Signature object

- `scheme` -- the verification scheme used to interpret the raw signature bytes.
- `signer` -- scheme-dependent signer metadata; for `SECP256K1` and `P256`, this is a 20-byte address.
- `msg` -- either empty, indicating the canonical transaction signature hash, or an explicit 32-byte digest.
- `signature` -- raw signature bytes interpreted according to `scheme`.

The `mode` of each frame sets the context of execution. It allows the protocol to identify
the purpose of the frame within the execution loop.

| `mode`  | Name           | Summary                                    |
|---------|----------------|--------------------------------------------|
| 0       | `DEFAULT` mode | Execute frame as `ENTRY_POINT`             |
| 1       | `VERIFY` mode  | Frame identifies as transaction validation |
| 2       | `SENDER` mode  | Execute frame as `sender`                  |


#### Constraints

Some validity constraints can be determined statically. They are outlined below:

```python
assert tx.chain_id < 2**256
assert tx.nonce < 2**64
assert tx.fees.max_priority_fee_per_gas < 2**256
assert tx.fees.max_fee_per_gas < 2**256
assert tx.fees.max_fee_per_blob_gas < 2**256
assert len(tx.frames) > 0 and len(tx.frames) <= MAX_FRAMES
assert len(tx.sender) == 20

for h in tx.blob_versioned_hashes:
    assert len(h) == 32 and h[0] == VERSIONED_HASH_VERSION_KZG
if len(tx.blob_versioned_hashes) == 0:
    assert tx.fees.max_fee_per_blob_gas == 0

for sig in tx.signatures:
    if sig.scheme in [SECP256K1, P256]:
        assert len(sig.signer) == 0 or len(sig.signer) == 20
    elif sig.scheme == ARBITRARY:
        assert len(sig.signer) == 0
    else:
        invalid_transaction()
    if len(sig.msg) == 0:
        assert sig.msg == Bytes()
    elif len(sig.msg) == 32:
        assert sig.msg != b"\x00" * 32
    else:
        invalid_transaction()

total_frame_gas = 0
total_frame_execution_gas = 0
for i, frame in enumerate(tx.frames):
    assert frame.mode < 3
    assert frame.flags < 8
    assert frame.target is None or len(frame.target) == 20
    assert frame.value < 2**256
    assert frame.mode == SENDER or frame.value == 0
    total_frame_execution_gas += frame.limits.execution
    total_frame_gas += frame.limits.execution + frame.limits.state
    assert total_frame_gas < 2**64

    # Approval of execution is allowed only when target equals to None or tx.sender.
    if frame.flags & APPROVE_EXECUTION:
        assert frame.target is None or frame.target == tx.sender

    # Atomic batch flag is only valid on non-VERIFY frames and requires
    # a subsequent non-VERIFY frame to batch with.
    if frame.flags & ATOMIC_BATCH_FLAG:
        assert frame.mode != VERIFY
        assert i + 1 < len(tx.frames)            # must not be last frame
        assert tx.frames[i + 1].mode != VERIFY   # batches never contain VERIFY frames

    # A frame belongs to a batch when it or its predecessor carries ATOMIC_BATCH_FLAG.
    if frame.flags & ATOMIC_BATCH_FLAG or (i > 0 and tx.frames[i - 1].flags & ATOMIC_BATCH_FLAG):
        assert frame.flags & APPROVE_SCOPE_MASK == 0

# Intrinsic and execution gas must fit the EIP-7825 transaction cap.
assert max(
    frame_tx_intrinsic_gas + total_frame_execution_gas,
    calldata_floor_gas,
) <= TX_MAX_GAS_LIMIT
```

#### Transaction Signatures

The `signatures` list contains signatures that may be referenced by `VERIFY` frames and by ordinary EVM execution. Every protocol-validated signature in this list must validate successfully before any frame is executed. If any signature is malformed, or any protocol-validated signature is invalid, the whole transaction is invalid.

The `signatures` list is optional. Contracts may still ignore it entirely and perform bespoke signature verification inside frame execution. Bespoke signature schemes must place their witness bytes in an `ARBITRARY` signature entry rather than in frame data when the witness signs the canonical transaction signature hash.

The raw `signature` byte strings of protocol-validated schemes are intentionally not introspectable by EVM code to allow future aggregation schemes. Contracts can inspect only the metadata of protocol-validated signature entries through transaction introspection. The raw `signature` byte strings of `ARBITRARY` entries are introspectable by EVM code through the `SIGDATACOPY` instruction. This means that they cannot be aggregated in the future.

The `scheme` identifies how the raw `signature` bytes are interpreted.

| `scheme`   | Name         | `signature` encoding                           | Gas cost |
|------------|--------------|------------------------------------------------|----------|
| 0x0        | `ARBITRARY`  | arbitrary bytes                                | 100      |
| 0x1        | `SECP256K1`  | `v (1 byte) || r (32 bytes) || s (32 bytes)`   | 2800     |
| 0x2        | `P256`       | `r || s || qx || qy` (32 bytes each)           | 6700     |
| 0x3..255   | reserved     | reserved                                       | reserved |

For `SECP256K1` and `P256`, the `signer` is a 20-byte Ethereum address. If absent, `tx.sender` is used (for introspections as well).
For `ARBITRARY`, `signer` MUST be empty. The protocol does not assign a resolved signer address to `ARBITRARY` entries.

The `msg` field defines which message the signature authorizes:

- if `len(msg) == 0`, the signature is signed over `compute_sig_hash(tx)`
- if `len(msg) == 32`, the signature is signed the explicit 32-byte digest `msg`
- any other `msg` length is invalid

The explicit 32-byte zero digest is invalid. This reserves the zero stack value as the EVM-visible representation of the transaction signing hash case.

For `P256`, the signer address must be `keccak256(qx || qy)[12:]`.

#### Receipt Encoding

The `ReceiptPayload` is defined as:

```
[cumulative_gas_used, payer, [frame_receipt, ...]]
frame_receipt = [status, gas_used, logs]
gas_used = [execution, state]
```

`payer` is the address of the account that paid the fees for the transaction. `status` is the return code of the top-level call. A new code `0x2` is introduced for frames which are skipped due to failed atomic batch. `gas_used` is the list of gas used by the frame, mirroring the frame's `limits` list. `gas_used.execution` is the execution gas used by the frame, not accounting for refunds. `gas_used.state` is the final state gas attributed to the frame after all state-gas refills and rollbacks in the transaction have been applied (see [Gas Accounting](#gas-accounting)). A later frame may therefore reduce an earlier frame's `gas_used.state`. The sum of all frame `gas_used.state` values is the transaction's final state-gas usage. `cumulative_gas_used` is still computed by adding the total gas used by the transaction with the previous cumulative sum. The transaction's logs, for the purposes of the block header `logsBloom` and log indexing, are the concatenation of the `logs` fields of its frame receipts, in frame order.

The `ReceiptPayload` carries no transaction-level status: the only statuses it holds are the per-frame ones. An interface that has to present a single status for the transaction therefore derives it, rather than reading it from the receipt.

#### Signature Hash

The canonical signature hash is defined such that any signature with empty `msg` will have its raw `signature` bytes elided.

```python
def compute_sig_hash(tx: FrameTx) -> Hash:
    for i, sig in enumerate(tx.signatures):
        if len(sig.msg) == 0:
            tx.signatures[i].signature = Bytes()
    return keccak(bytes([FRAME_TX_TYPE]) + rlp(tx))
```

#### Signature Validation

Every signature in the outer transaction object is validated or structurally checked before frame execution. The validation rules are:

```python
ARBITRARY = 0x0
SECP256K1 = 0x1
P256      = 0x2

def validate_signature(sig, tx_sender, sig_hash) -> bool:
    if len(sig.msg) == 0:
        msg = sig_hash
    elif len(sig.msg) == 32:
        if sig.msg == b"\x00" * 32:
            return False
        msg = sig.msg
    else:
        return False
    
    if len(sig.signer) == 0:
        resolved_signer = tx_sender
    elif len(sig.signer) == 20:
        resolved_signer = sig.signer
    else:
        return False

    if sig.scheme == SECP256K1:
        if len(sig.signature) != 65:
            return False
        v = sig.signature[0]
        r = int.from_bytes(sig.signature[1:33], "big")
        s = int.from_bytes(sig.signature[33:65], "big")
        # v is the recovery id (0 or 1), matching EIP-2718 typed transactions.
        # r and s must be canonical, with low-s per EIP-2, so each signature has one encoding.
        if v > 1 or not (0 < r < SECP256K1N) or not (0 < s <= SECP256K1N // 2):
            return False
        return resolved_signer == ecrecover(msg, v, r, s)

    elif sig.scheme == P256:
        if len(sig.signature) != 128:
            return False
        r  = int.from_bytes(sig.signature[0:32], "big")
        s  = int.from_bytes(sig.signature[32:64], "big")
        qx = sig.signature[64:96]
        qy = sig.signature[96:128]
        # r and s must be canonical, with low-s, so each signature has one encoding.
        # P256VERIFY itself accepts high-s signatures, so signers that produce a high-s value must normalize it to SECP256R1N - s before use.
        if not (0 < r < SECP256R1N) or not (0 < s <= SECP256R1N // 2):
            return False
        if resolved_signer != keccak256(qx + qy)[12:]:
            return False
        return P256VERIFY(msg, r, s, qx, qy)

    elif sig.scheme == ARBITRARY:
        return len(sig.signer) == 0

    else:
        return False
```

Note: since the signature validation does not happen in EVM execution, the related precompiles `ecrecover` and `P256VERIFY` must not be added to the block-level access list.

#### Expiry Verifier Frame

A `VERIFY` frame whose `frame.target` equals `EXPIRY_VERIFIER` is an **expiry verifier frame**. It calls the expiry verifier contract deployed at `EXPIRY_VERIFIER` with `frame.data` as calldata. The calldata is interpreted as an 8-byte unsigned big-endian expiry timestamp. The call reverts unless `block.timestamp <= expiry_timestamp`.

An expiry verifier frame is invalid unless all of the following hold:

- `frame.flags == 0`
- `frame.value == 0`
- `frame.limits.state == 0`
- `len(frame.data) == EXPIRY_DATA_LENGTH`

A transaction can contain at most one expiry verifier frame.

At activation, clients must install the following expiry verifier contract runtime code at `EXPIRY_VERIFIER`. The expiry verifier contract's runtime code must be:

```asm
push1 0x08
calldatasize
eq
push1 0x0a
jumpi
push0
push0
revert

jumpdest
push0
calldataload
push1 0xc0
shr
timestamp
gt
push1 0x16
jumpi
stop

jumpdest
push0
push0
revert
```

The runtime bytecode is:

```text
0x60083614600a575f5ffd5b5f3560c01c4211601657005b5f5ffd
```

Equivalently, the runtime behavior is:

```python
if len(evm.calldata) != EXPIRY_DATA_LENGTH:
    revert()

expiry_timestamp = int.from_bytes(evm.calldata, "big")
if evm.timestamp > expiry_timestamp:
    revert()

stop()
```

If the check passes, the frame succeeds with no return data and no logs. The frame consumes gas according to normal EVM execution rules.

Clients may omit the explicit EVM execution and directly perform the expiry check above, provided the externally observable result is identical to executing the canonical contract. Note: While this is a valid optimization for Ethereum mainnet, it could be problematic on non-mainnet situations in case a different contract is used.

#### Behavior

When processing a frame transaction, perform the following steps.

To begin processing a frame transaction:

1. Ensure `tx.nonce == state[tx.sender].nonce`
1. Compute the canonical transaction signature hash: `sig_hash = compute_sig_hash(tx)`.
1. For each `sig` in `tx.signatures`, ensure `validate_signature(sig, tx.sender, sig_hash) == true`.
1. Initialize transaction-scoped variables:
    - `payer = None`
    - `sender_approved = false`

Then for each frame:

1. Execute a call with the specified `mode`, `flags`, `target`, `limits`, `value`, and `data`.
    - Initialize the frame's receipt with `gas_used = [0, 0]`.
    - At frame entry, initialize the frame's gas pools per [Gas Accounting](#gas-accounting): `gas_left = frame.limits.execution` and `state_gas_left = frame.limits.state`.
    - Let `resolved_target = frame.target if frame.target is not None else tx.sender`
        - Unless otherwise stated, checks that refer to the target account during execution use the resolved target.
    - Charge the resolved target's warm or cold account access ([EIP-2929](./eip-2929.md)) from `gas_left`, before the balance check and before dispatch. Resolving the target's code is how the frame is dispatched, so the charge applies uniformly, whether the frame goes on to run contract code, delegated code, or the [default code](#default-code). If `gas_left` cannot cover the charge, the frame halts exceptionally.
    - Set frame's `caller`:
        - If mode is `SENDER`:
            - `sender_approved` must be `true`. If not, the transaction is invalid.
            - Set `caller` to `tx.sender`.
        - If mode is `DEFAULT` or `VERIFY`:
            - Set `caller` to `ENTRY_POINT`.
        - The `ORIGIN` opcode returns frame's `caller` throughout all call depths.
    - In the top-level frame call, `CALLVALUE` is `frame.value`.
        - As with an ordinary `CALL`, if the caller does not have sufficient balance to transfer `frame.value`, the frame reverts, consuming the gas charged so far.
        - If `frame.value > 0` and `resolved_target` does not exist, `STATE_BYTES_PER_NEW_ACCOUNT * CPSB` state gas is charged after the balance check and before the frame's code executes, as in [EIP-2780](./eip-2780.md). If `state_gas_left` cannot cover the charge, the frame halts exceptionally.
        - A non-zero value transfer to an address other than `tx.sender` emits the transfer log specified by [EIP-7708](./eip-7708.md).
    - If `resolved_target` is an active precompile at the current fork, the frame dispatches it as an ordinary call would.
    - Otherwise, if `resolved_target` code hash is empty, i.e. `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`, execute the logic described in [default code](#default-code).
        - Otherwise, if `resolved_target` uses an [EIP-7702](./eip-7702.md) delegation indicator, execute according to [EIP-7702](./eip-7702.md)'s delegated-code semantics.
    - If a frame's execution reverts, its state changes and approval context (`payer`, `sender_approved`) are discarded. Additionally, if this frame has the atomic batch flag set, mark all subsequent frames in the same atomic group as skipped.
1. If frame has mode `VERIFY` the following additional requirements are imposed:
    - Execute the frame as a `STATICCALL`, disallowing state manipulation.
        - Only `APPROVE` can modify the state or transaction context in `VERIFY`.
    - If the frame fails by reverting or halting exceptionally, the transaction is invalid. This unrolls any effects of `APPROVE`.
1. If a frame is part of an atomic batch and it fails, unroll the associated atomic batch.
    - An **atomic batch** is a maximal contiguous sequence of frames `[i, j]` where `j > i`, frames `i` through `j - 1` have `ATOMIC_BATCH_FLAG` set, and frame `j` does not have `ATOMIC_BATCH_FLAG` set.
    - When a frame in the batch fails, the state must be rolled back to the condition it was immediately before the atomic batch began. All remaining frames in the atomic batch are skipped.
    - Logs emitted by frames that executed before the failure are discarded together with their state changes when the batch is unrolled. Changes to any frame receipt's `gas_used.state` during the batch are reverted, as the batch's state modifications are reverted. Executed frame receipts retain their execution status and execution gas used, with empty logs and zero state gas used.
    - Since skipped frames are not executed, the execution and state gas allotted to them are refunded at the end of the transaction.
    - `APPROVE` is not possible within a batch, so it is not necessary to rollback anything related to `payer` or `sender_approved`.
    - If the frame does not have an associated atomic batch, further special handling is not needed, simply revert the individual frame.

After executing all frames, verify that `payer` has been set (i.e. `payer != None`). If it is not, the whole transaction is invalid. Then, settle fees as defined in [Gas Accounting](#gas-accounting), returning `payer_refund` to the payer.

##### Cross-frame interactions

A few cross-frame interactions to note:

- When frame-transaction processing begins, `accessed_addresses` is initialized as in [EIP-2929](./eip-2929.md) and [EIP-3651](./eip-3651.md) (`tx.sender`, the coinbase, and precompiles warm), and `accessed_storage_keys` empty (there is no access list). Being a frame target does not warm an address: a `resolved_target`'s warm/cold access is charged at frame entry within the frame's own `limits.execution`. The payer needs no separate warming rule: it is the executing frame's `resolved_target`, whose access was charged at frame entry and journaled like any other [EIP-2929](./eip-2929.md) access. `ENTRY_POINT` is not pre-warmed, so `ORIGIN` is cold in `DEFAULT` and `VERIFY` frames (where it is `ENTRY_POINT`) and warm in a `SENDER` frame (where it is `tx.sender`).
- For the purposes of gas accounting of warm / cold state status, the journal of such touches is shared across frames.
- If a frame reverts, warm / cold status reverts to the state before the frame.
- State-gas ownership records and changes to frame receipts' `gas_used.state` fields journaled alongside the state changes that produced them. Reverting a call, frame, or atomic batch restores all of them to the corresponding checkpoint.
- Discard the `TSTORE` and `TLOAD` transient storage between frames.

##### Gas Accounting

Frame transactions adopt the two-dimensional gas model of [EIP-8037](./eip-8037.md): **execution gas** meters computation, data, and account access, while **state gas** meters durable state growth. Where EIP-8037 retrofits the split onto envelopes carrying a single gas field via the reservoir model, the frame transaction declares both budgets explicitly: `frame.limits.execution` bounds a frame's execution gas and `frame.limits.state` bounds its state gas. The reservoir model — including the `state_gas_reservoir` split and the `state_gas_from_gas_left` accounting — does not apply within frame transactions.

The total gas limit of the transaction is:

```python
def signature_gas(sig):
    if sig.scheme == SECP256K1:
        return 2800
    if sig.scheme == P256:
        return 6700
    if sig.scheme == ARBITRARY:
        return 100
    invalid_transaction()

def tokens_in(data):
    zero_bytes = data.count(b"\x00")
    nonzero_bytes = len(data) - zero_bytes
    return zero_bytes + nonzero_bytes * 4

def floor_tokens_in(data):
    return len(data) * 4

def calldata_cost(data):
    return STANDARD_TOKEN_COST * tokens_in(data)

def value_cost(frame):
    if frame.value > 0 and frame.target is not None and frame.target != tx.sender:
        return TX_VALUE_COST
    return 0

signature_verification_cost = sum(signature_gas(sig) for sig in tx.signatures)

frame_data_cost = sum(calldata_cost(frame.data) for frame in tx.frames)
signature_data_cost = sum(
    calldata_cost(sig.signer) + calldata_cost(sig.msg) + calldata_cost(sig.signature)
    for sig in tx.signatures
)
value_transfer_cost = sum(value_cost(frame) for frame in tx.frames)

frame_tx_intrinsic_gas = (
    FRAME_TX_INTRINSIC_COST
    + len(tx.frames) * FRAME_TX_PER_FRAME_COST
    + frame_data_cost
    + signature_data_cost
    + signature_verification_cost
    + value_transfer_cost
)

standard_gas_limit = (
    frame_tx_intrinsic_gas
    + sum(frame.limits.execution for frame in tx.frames)
    + sum(frame.limits.state for frame in tx.frames)
)

calldata_floor_tokens = (
    sum(floor_tokens_in(frame.data) for frame in tx.frames)
    + sum(floor_tokens_in(sig.signer) + floor_tokens_in(sig.msg) + floor_tokens_in(sig.signature) for sig in tx.signatures)
)

calldata_floor_gas = (
    FRAME_TX_INTRINSIC_COST
    + len(tx.frames) * FRAME_TX_PER_FRAME_COST
    + signature_verification_cost
    + value_transfer_cost
    + TOTAL_COST_FLOOR_PER_TOKEN * calldata_floor_tokens
)

max_gas = max(standard_gas_limit, calldata_floor_gas + sum(frame.limits.state for frame in tx.frames))
```

`frame_tx_intrinsic_gas` is the transaction's intrinsic cost in the sense of [EIP-2780](./eip-2780.md). It is derivable from the transaction fields alone, with no state access. It is charged entirely in the execution dimension and, together with the sum of the declared frame execution-gas limits, is limited by the [EIP-7825](./eip-7825.md) cap explained in [Constraints](#constraints). `value_transfer_cost` charges `TX_VALUE_COST` for each frame which carries value, covering the recipient balance write and transfer log exactly as EIP-2780 prices top-level value transfers. State dependent costs do not enter this calculation and are charged at runtime inside frame state-gas budgets, as described below. The two token counts differ as in [EIP-7976](./eip-7976.md): the standard intrinsic prices the weighted count (`tokens_in`), while the floor prices every charged byte uniformly (`floor_tokens_in`).

###### Frame gas pools

Each frame executes against two pools, initialized at frame entry:

```python
gas_left = frame.limits.execution
state_gas_left = frame.limits.state
```

The pools are independent:

- Execution-gas draws from `gas_left`, under unchanged EVM semantics. `gas_left` is the standard callframe gas pool. The `GAS` instruction returns the current callframe's `gas_left` and never observes `state_gas_left`. The `GAS_CALL_STIPEND` pre-state check of EIP-8037 applies to `gas_left` only.
- State-gas draws from `state_gas_left` only. `state_gas_left` is frame-scoped. EVM call frames at any depth within the frame draw from it directly. It is not forwarded, split, or divided per callframe like execution gas.
- A charge exceeding its remaining pool is an exceptional halt of the current callframe, with ordinary out-of-gas semantics. Execution gas can never be spent on state charges.

Charge points follow EIP-8037 unchanged. `SSTORE` state gas at the end of the opcode, the conditional account-creation charge of the `CALL*` and `CREATE`/`CREATE2` families in the creating frame, and code-deposit gas at deposit time. In addition, the frame-level account creation charge for value carrying frames ([Behavior](#behavior)) and the account creation charge applied by [`APPROVE`](#approve-instruction-0xaa) mirror EIP-2780's runtime account creation charges. A charge that exhausts either pool halts the current call frame rather than changing the transaction's static validity. As specified above, failure of a `VERIFY` frame nevertheless makes the transaction invalid.

###### State-gas attribution and refills

Every successful state-gas charge is attributed to the currently executing frame. After deducting the amount from `state_gas_left`, add it to that frame's receipt:

```python
state_gas_left -= amount
frame_receipts[current_frame_index].gas_used.state += amount
```

The receipt mutation is journaled together with the state creation that caused it. Journal entries are retained after a frame completes so that a later refill or atomic-batch rollback can update that frame's receipt. For an `SSTORE` charge, also journal the charging frame's index as the outstanding charge owner for that `(address, storage_key)`. The refill conditions of EIP-8037 apply unchanged, with *original value* meaning the slot value at the start of the transaction.

When a refill fires, subtract the amount from the receipt of the frame that paid the outstanding charge:

```python
owner = outstanding_charge.frame_index
frame_receipts[owner].gas_used.state -= amount
```

The destination of the spendable refill remains frame-isolated:

- If `owner == current_frame_index`, also credit the amount to the current frame's `state_gas_left`. A frame's spendable refills can never exceed its own outstanding charges, so `state_gas_left` never exceeds `frame.limits.state`.
- If `owner != current_frame_index`, reduce the owner frame's `gas_used.state` to return the correct amount during transaction settlement.

In both cases the storage slot's ownership entry is cleared, so a subsequent creation there is charged and attributed to whichever frame performs it.

All changes to `state_gas_left`, outstanding charge ownership, and frame receipts' `gas_used.state` fields are journaled at the same rollback boundaries as the associated state changes:

- When an EVM call frame reverts or halts exceptionally, restore the state-gas journal and the active frame's `state_gas_left` to the checkpoint covering that call and any associated pre-call state charge.
- When a frame reverts, restore the journal and `state_gas_left` to frame entry. Its final `gas_used.state` is therefore zero, and any changes it made to earlier receipts are undone. Both remaining pools count toward `tx_unused_gas`.
- When a frame halts exceptionally, perform the same state-gas restoration, but consume its entire execution-gas pool. Its final `gas_used.state` is zero.
- When an atomic batch is unrolled, restore the state-gas journal to the batch-entry checkpoint. Charges attributed to frames in the batch are removed from their receipts, while refills of charges attributed to frames before the batch are undone. Closed frame pools are not reopened.

EIP-8037 assigns no state-gas refill to `SELFDESTRUCT`, including when it deletes a contract created earlier in the transaction. Such an operation therefore does not modify the creating frame's `gas_used.state`.

###### Transaction settlement

Each frame's receipt reports its execution-gas usage measured at frame exit:

```python
frame_receipt.gas_used.execution = frame.limits.execution - gas_left
```

At frame exit, `frame_receipt.gas_used.state` equals `frame.limits.state - state_gas_left`. It remains subject to journaled changes caused by later frames. After all frames and atomic rollbacks have completed, it is the frame's final attributed state gas. The sum of frame execution and state usage does not generally equal the transaction's `gas_used`: intrinsic costs are charged outside frame budgets, while the storage refund and calldata floor apply at the transaction level.

Gas not charged at settlement includes execution and state gas left in frame pools at frame exit, skipped-frame budgets, and state gas removed from a receipt by a later refill or rollback. None of this gas becomes available to a subsequent frame. Let `frame_receipts` be the ordered frame receipt list. After all frames, the total amount not charged can be calculated as:

```python
tx_unused_gas = sum(
    (frame.limits.execution - frame_receipt.gas_used.execution)
    + (frame.limits.state - frame_receipt.gas_used.state)
    for frame, frame_receipt in zip(tx.frames, frame_receipts)
)
```

Storage gas refunds ([EIP-3529](./eip-3529.md)) accumulate across frames into a single transaction-scoped `refund_counter`. Changes made to the counter by a reverted frame, or by frames unrolled as part of a failed atomic batch, are discarded together with that frame's state changes. State-gas refills have already reduced the owning frame receipts before settlement. At the end of the transaction, apply the storage refund and the [EIP-7623](./eip-7623.md) calldata floor — compared against the transaction's execution component — to get the final `gas_used` value:

```python
gas_used_before_refund = standard_gas_limit - tx_unused_gas
applied_refund = min(refund_counter, gas_used_before_refund // MAX_REFUND_QUOTIENT)
gas_used_after_refund = gas_used_before_refund - applied_refund

tx_state_gas = sum(fr.gas_used.state for fr in frame_receipts)
tx_execution_gas = max(gas_used_after_refund - tx_state_gas, calldata_floor_gas)

gas_used = tx_execution_gas + tx_state_gas
```

`gas_used` is by construction the sum of the transaction's two block-accounted dimensions. When the calldata floor binds, it exceeds EIP-7623's transaction-total floor: state gas never absorbs into the data floor (see [Calldata floor and payment](#calldata-floor-and-payment)).

Because a state-gas refill directly reduces `frame_receipt.gas_used.state`, it reduces `gas_used_before_refund` and is not subject to the refund cap. Per EIP-8037, a refill reverses a charge for state that was never durably created, while a refund rebates work that was actually performed.

The payer's refund is then computed below using the final `gas_used` value:

```python
blob_gas = len(blob_versioned_hashes) * GAS_PER_BLOB
max_cost = (
    max_gas * tx.fees.max_fee_per_gas
    + blob_gas * blob_base_fee
)
assert max_cost < 2**256
charged_fee = gas_used * effective_gas_price + blob_gas * blob_base_fee
payer_refund = max_cost - charged_fee
```

After execution, return `payer_refund` to the payer. This is the `resolved_target` that called `APPROVE(APPROVE_PAYMENT)` or `APPROVE(APPROVE_EXECUTION_AND_PAYMENT)`.

###### Block-level gas accounting

Blocks account for the two dimensions separately, per EIP-8037. Because this EIP requires [EIP-7778](./eip-7778.md), the storage refund does not reduce the gas counted toward the block gas limit, so the execution dimension is accounted before the refund. This is the EIP-8037 EIP-7778 integration applied to the frame transaction's explicit dimensions: the state dimension stays net, since a state-gas refill reverses a charge for state that was never durably created rather than rebating performed work.

```python
block_execution_gas = max(gas_used_before_refund - tx_state_gas, calldata_floor_gas)

block_output.block_execution_gas_used += block_execution_gas
block_output.block_state_gas_used += tx_state_gas
```

All intrinsic costs are execution gas, so the execution dimension is the transaction total less its net state gas. The payer is charged `gas_used = tx_execution_gas + tx_state_gas` (post-refund), while the block counts `block_execution_gas + tx_state_gas` (execution pre-refund): the refund lowers the payer's cost without freeing the block capacity the transaction occupied, exactly as EIP-7778 intends. The block header `gas_used`, the block validity condition, and the base fee update rule follow EIP-8037 unchanged.

A frame transaction may be included in a block only if its worst case fits the remaining capacity of **each** dimension:

```python
execution_reservation = max(
    frame_tx_intrinsic_gas + sum(frame.limits.execution for frame in tx.frames),
    calldata_floor_gas,
)
state_reservation = sum(frame.limits.state for frame in tx.frames)

assert execution_reservation <= block_gas_limit - block_output.block_execution_gas_used
assert state_reservation <= block_gas_limit - block_output.block_state_gas_used
```

Because both budgets are explicit, these reservations are exact per dimension; no portion of the transaction's gas is reserved in both dimensions at once, as it must be under EIP-8037's reservoir model.

##### Blob handling

When `blob_versioned_hashes` is non-empty, the transaction is a blob-carrying transaction and follows [EIP-4844](./eip-4844.md), where the payer is also the blob-fee payer. The following distinctions from EIP-4844 must also be adhered to:

- The transaction is only valid for inclusion in a block if `tx.fees.max_fee_per_blob_gas >= blob_base_fee` of that block. `max_fee_per_blob_gas` is used only for this inclusion check. The payer is charged `blob_gas * blob_base_fee`; no additional blob fee is collected or refunded.
- It contributes `len(blob_versioned_hashes) * GAS_PER_BLOB` to the block's `blob_gas_used` and counts against the blob limits of the active fork. The per-transaction blob limit of [EIP-7594](./eip-7594.md) applies unchanged.
- Unlike EIP-4844 transactions, a frame transaction is not required to carry blobs.
- The `BLOBHASH` instruction returns `tx.blob_versioned_hashes[index]` in every frame, per EIP-4844.

##### Default code

Frame transactions can be used by accounts who do not have deployed code, nor an [EIP-7702](./eip-7702.md) delegation indicator via the "default code" mechanism. The behavior of the default code is defined below:

- If `mode` is `VERIFY`:
  - Read the allowed approval scope from the flags field: `allowed_scope = frame.flags & APPROVE_SCOPE_MASK`.
  - If `allowed_scope == APPROVE_SCOPE_NONE`, revert.
  - Let `sig_index = 0` if `allowed_scope & APPROVE_EXECUTION != 0`, else `sig_index = 1`.
  - If there is not a `SECP256K1` signature `sig` at index `sig_index` such that `resolved_signer == resolved_target` and `sig.msg == Bytes()`, revert.
  - Call `APPROVE(allowed_scope)`.
- If `mode` is `SENDER` or `DEFAULT`:
  - Return successfully as if calling empty code.

The default code draws no execution gas of its own: the frame's only execution charge is the resolved target's access, taken at frame entry ([Behavior](#behavior)). Its `APPROVE` may charge state gas for sender-account creation, as specified in the [`APPROVE`](#approve-instruction-0xaa) instruction's behavior. A frame whose `limits.execution` cannot cover the entry access charge halts exceptionally before the default code is evaluated — which, for a `VERIFY` frame, invalidates the transaction.

### `APPROVE` Instruction (`0xaa`)

The `APPROVE` instruction exits the current EVM call frame successfully and updates the transaction-scoped approval context based on the `scope` operand. The `offset` and `length` operands designate a memory region that becomes the call frame's return data, following `RETURN` semantics.

#### Stack

| Stack      | Value        |
| ---------- | ------------ |
| `top - 0`  | `offset`     |
| `top - 1`  | `length`     |
| `top - 2`  | `scope`      |

#### Scope Operand

The `scope` operand is a bitmask. Define the following constants:

- `APPROVE_NONE` (`0x0`): No approval scope allowed - the current frame is not allowed to approve any scope.
- `APPROVE_PAYMENT` (`0x1`): Approval of payment - the contract approves paying the total gas cost for the transaction.
- `APPROVE_EXECUTION` (`0x2`): Approval of execution - the sender contract approves future frames calling on its behalf.
    - Note this is only valid when `resolved_target` equals `tx.sender`.
- `APPROVE_EXECUTION_AND_PAYMENT` (`0x3`): Approval of payment and execution.

`APPROVE_SCOPE_MASK` is defined as an alias for `APPROVE_EXECUTION_AND_PAYMENT`.

#### Behavior

The behavior of `APPROVE` is defined as follows:

- If the current transaction is not a frame transaction, an exceptional halt occurs.
- If `ADDRESS != resolved_target`, revert.
- Ensure that `scope` is one of the caller's allowed scopes in `frame.flags`, otherwise revert.
    - To check this, evaluate that `scope != 0` and `scope & ~(frame.flags & APPROVE_SCOPE_MASK) == 0`.
- Depending on the value of `scope`, perform the following:
    - For `APPROVE_EXECUTION`:
        - If `sender_approved == true`, revert the current call frame.
        - If `resolved_target != tx.sender`, revert the current call frame.
        - Set `sender_approved = true`.
    - For `APPROVE_PAYMENT`
        - If `payer` was already set, revert the current call frame.
        - If `resolved_target` has insufficient balance, revert the current call frame.
        - If `sender_approved == false`, revert the current call frame.
        - Immediately before incrementing the sender's nonce, if `tx.sender` does not exist under the existence rule of [EIP-8037](./eip-8037.md), charge `STATE_BYTES_PER_NEW_ACCOUNT * CPSB` from the current frame's `state_gas_left`. If the pool cannot cover the charge, halt the current call frame exceptionally without applying any approval effects.
        - Increment the sender's nonce, set `payer = resolved_target`, and collect the transaction's `max_cost` from `payer`.
    - For `APPROVE_EXECUTION_AND_PAYMENT`:
        - If `sender_approved` or `payer` was already set, revert the current call frame.
        - If `resolved_target` != `tx.sender`, revert the current call frame.
        - If `resolved_target` has insufficient balance, revert the current call frame.
        - Immediately before incrementing the sender's nonce, if `tx.sender` does not exist under the existence rule of [EIP-8037](./eip-8037.md), charge `STATE_BYTES_PER_NEW_ACCOUNT * CPSB` from the current frame's `state_gas_left`. If the pool cannot cover the charge, halt the current call frame exceptionally without applying any approval effects.
        - Set `sender_approved = true`, increment the sender's nonce, set `payer = resolved_target`, and collect the transaction's `max_cost` from `payer`.

#### Gas

`APPROVE` charges the memory expansion gas for the return-data region `[offset, offset + length)`, with no additional execution-gas base cost, matching `RETURN`. Updating the transaction-scoped approval context has no additional execution-gas cost: its once-per-transaction effects — the sender nonce increment, setting `payer`, and collecting the maximum cost — are already covered by the frame transaction's intrinsic cost, exactly as they are for a standard transaction. If incrementing the nonce creates the sender account, the state-gas charge specified above applies in addition.

### Introspection

The instructions in this section provide the needed introspection capabilities of the new frame transaction type and the frame objects themselves, including some runtime information like the execution result status.

The instructions in this section are defined only during the execution of a frame transaction. Executing any of them in the context of any other transaction type results in an exceptional halt.

#### `TXPARAM` Instruction `(0xb0)`

This instruction gives access to transaction-scoped information. The gas cost of this operation is `2`. It takes one value from the stack, `param`, and returns the associated transaction value to the stack. See the table for the full mapping.

| `param` | Return value                                                                |
|---------|-----------------------------------------------------------------------------|
| 0x00    | current transaction type                                                    |
| 0x01    | `nonce`                                                                     |
| 0x02    | `sender`                                                                    |
| 0x03    | `fees.max_priority_fee_per_gas`                                             |
| 0x04    | `fees.max_fee_per_gas`                                                      |
| 0x05    | `fees.max_fee_per_blob_gas`                                                 |
| 0x06    | max cost (basefee=max, all gas used, includes blob cost at `blob_base_fee`, intrinsic cost, and signature verification cost) |
| 0x07    | `len(blob_versioned_hashes)`                                                |
| 0x08    | `compute_sig_hash(tx)`                                                      |
| 0x09    | `len(frames)`                                                               |
| 0x0A    | currently executing frame index                                             |
| 0x0B    | `len(signatures)`                                                           |
| 0x0C    | `state_gas_left` remaining in the currently executing frame                |


Undefined `param` values result in an exceptional halt.

#### `FRAMEDATALOAD` Instruction `0xb1`

This opcode loads one 32-byte word of data from frame input. Gas cost: 3 (matches CALLDATALOAD).

It takes two values from the stack and places the retrieved word on the stack:

| Stack     | Value        |
| --------- | ------------ |
| `top - 0` | `offset`     |
| `top - 1` | `frameIndex` |

When the `frameIndex` is out-of-bounds, an exceptional halt occurs.

The operation semantics match CALLDATALOAD, returning a word of data from the chosen
frame's `data`, starting at the given byte `offset`.

#### `FRAMEDATACOPY` Instruction (`0xb2`)

This opcode copies data frame input into the contract's memory. Its gas cost is calculated
exactly as for `CALLDATACOPY`, including the fixed cost of 3, the per-word copy cost, and
the standard EVM memory expansion cost.

It takes four values from the stack and produces no output:

| Stack     | Value        |
| --------- | ------------ |
| `top - 0` | `memOffset`  |
| `top - 1` | `dataOffset` |
| `top - 2` | `length`     |
| `top - 3` | `frameIndex` |

When the `frameIndex` is out-of-bounds, an exceptional halt occurs.

The operation semantics match CALLDATACOPY, copying `length` bytes from the chosen frame's
`data`, starting at the given byte `dataOffset`, into a memory region starting at
`memOffset`.

#### `FRAMEPARAM` Instruction (`0xb3`)

This instruction gives access to frame-scoped information. The gas cost of this operation is `2`. It takes two values from the stack, `frameIndex` on top and `param` second from top.

| `param` | `frameIndex` | Return value                                              |
|---------|--------------|-----------------------------------------------------------|
| 0x00    | frameIndex   | `resolved_target`                                         |
| 0x01    | frameIndex   | `limits.execution`                                        |
| 0x02    | frameIndex   | `mode`                                                    |
| 0x03    | frameIndex   | `flags`                                                   |
| 0x04    | frameIndex   | `len(data)`                                               |
| 0x05    | frameIndex   | `status` (exceptional halt if current/future)             |
| 0x06    | frameIndex   | `allowed_scope` (`frame.flags & APPROVE_SCOPE_MASK`)      |
| 0x07    | frameIndex   | `atomic_batch` (`(frame.flags >> 2) & 0x01`, returns 0/1) |
| 0x08    | frameIndex   | `value`                                                   |
| 0x09    | frameIndex   | `limits.state`                                            |
| 0x0A    | frameIndex   | `gas_used.execution` (exceptional halt if current/future) |
| 0x0B    | frameIndex   | `gas_used.state` (exceptional halt if current/future)     |

Notes:

- The `status` field (0x05) returns `0` for failure, `1` for success, or `2` for a frame skipped due to a failed atomic batch.
- The `gas_used.execution` (0x0A) and `gas_used.state` (0x0B) fields return the current values recorded in the frame's receipt. Like `status`, they are defined only for frames that have completed; accessing them for the current frame or a subsequent frame results in an exceptional halt. A completed frame's `gas_used.state` may subsequently decrease when another frame refills state gas attributed to it, or be restored if that later change is rolled back. `gas_used.execution` does not change after frame completion.
- Undefined `param` values result in an exceptional halt.
- Out-of-bounds access for `frameIndex` results in an exceptional halt.
- Attempting to access the return `status` of the current frame or a subsequent frame results in an exceptional halt.


#### `SIGPARAM` Instruction (`0xb4`)

This instruction gives access to signature-scoped metadata. The gas cost of this operation is `2`. It takes two values from the stack, `signatureIndex` on top and `param` second from top, and returns the associated value to the stack.

| `param` | `signatureIndex` | Return value       |
|---------|------------------|--------------------|
| 0x00    | signatureIndex   | `resolved_signer`  |
| 0x01    | signatureIndex   | `scheme`           |
| 0x02    | signatureIndex   | `msg`              |
| 0x03    | signatureIndex   | `len(signature)` (`ARBITRARY` only) |

Notes:

- Undefined `param` values result in an exceptional halt.
- Out-of-bounds access for `signatureIndex` results in an exceptional halt.
- For `ARBITRARY` signature entries, requesting `resolved_signer` results in an exceptional halt.
- For signature entries of any other `scheme`, requesting `len(signature)` results in an exceptional halt. The raw `signature` bytes of protocol-validated schemes, including their length, are not introspectable.

#### `SIGDATACOPY` Instruction (`0xb5`)

This opcode copies a signature's raw `signature` bytes into the contract's memory. The raw `signature` bytes of protocol-validated schemes are intentionally not accessible from the EVM. `ARBITRARY` signature bytes are validated during EVM execution and therefore may be copied. Its gas cost is calculated exactly as for `CALLDATACOPY`, including the fixed cost of 3, the per-word copy cost, and the standard EVM memory expansion cost.

It takes four values from the stack and produces no output:

| Stack     | Value            |
| --------- | ---------------- |
| `top - 0` | `memOffset`      |
| `top - 1` | `dataOffset`     |
| `top - 2` | `length`         |
| `top - 3` | `signatureIndex` |

When the `signatureIndex` is out-of-bounds, an exceptional halt occurs. If the referenced signature entry's `scheme` is not `ARBITRARY`, an exceptional halt occurs.

The operation semantics match `CALLDATACOPY`, copying `length` bytes from the chosen signature's raw `signature` bytes, starting at the given byte `dataOffset`, into a memory region starting at `memOffset`. Bytes beyond the end of the signature are copied as zeroes.

The byte length of an `ARBITRARY` signature entry is available via `SIGPARAM` with `param == 0x03`.

### Mempool

The transaction mempool must carefully handle frame transactions, as a naive implementation could introduce denial-of-service vulnerabilities. The fundamental goal of the public mempool rules is to avoid allowing an arbitrary number of transactions to be invalidated by a single environmental change or state modification. Beyond this, the rules also aim to minimize the amount of work needed to complete the initial validation phase of a transaction before an acceptance decision can be made.

This policy is inspired by [ERC-7562](./eip-7562.md), but removes staking and reputation entirely. Any behavior that ERC-7562 would admit only for a staked or reputable third party is rejected here for the public mempool. Transactions outside these rules may be accepted into a local or private mempool, but must not be propagated through the public mempool.

#### Constants

| Name  | Value  | Description  |
|---|---|---|
| `MAX_VERIFY_GAS`   | `100_000`  | Maximum amount of gas a node should expend validating signatures and simulating the validation prefix |
| `MAX_VERIFY_STATE_GAS`   | `500_000`  | Maximum amount of state gas that may be budgeted across the validation prefix |
| `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER`   | `1`  | Maximum amount of pending transactions that can be using any given non-canonical paymaster |

#### Validation Prefix

The **validation prefix** of a frame transaction is the shortest prefix of frames whose successful execution sets `payer`.

Public mempool rules apply only to the validation prefix. Once `payer` has been set, subsequent frames are outside public mempool validation and may be arbitrary. In particular, `user_op` and `post_op` occur after payment approval and are therefore not subject to the public mempool restrictions below.

For public mempool accounting, signature validation is treated as part of the transaction intrinsic cost and counts against `MAX_VERIFY_GAS`.

#### Policy Summary

A frame transaction is eligible for public mempool propagation only if its validation prefix depends exclusively on:

1. transaction fields, including the canonical signature hash,
2. the block timestamp as read by an expiry verifier frame,
3. the sender's nonce, code, and storage,
4. if a deploy frame is present, the code of the factory targeted by that frame (subject to the deploy-frame trace rules below),
5. if a paymaster frame is present, either a canonical paymaster instance together with explicit paymaster balance reservation, or a non-canonical paymaster being used by less than `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER` pending transactions,
6. the code of any other existing non-delegated contracts reached during validation via `CALL*` or `EXTCODE*`, provided the resulting trace does not access disallowed mutable state.

Any dependency on third-party mutable state outside these categories must result in rejection by the public mempool.

#### Mode Subclassifications

While the frames are designed to be generic, we refine some frame modes for the purpose of specifying public mempool handling clearly.

| Name | Mode | Flags | Description |
|---|---|---|---|
| `self_verify` | VERIFY | `0x3` | Validates the transaction and approves both sender and payer |
| `deploy` | DEFAULT | `0x0` | Deploys a new smart account, typically via a deterministic factory such as the [EIP-7997](./eip-7997.md) predeploy |
| `only_verify` | VERIFY | `0x2` | Validates the transaction and approves only the sender |
| `pay` | VERIFY | `0x1` | Validates the transaction and approves only the payer |
| `expiry_verify` | VERIFY | `0x0` | Calls the expiry verifier contract (target = `EXPIRY_VERIFIER`) |
| `user_op` | SENDER | any | Executes the intended user operation |
| `post_op` | DEFAULT | any | Executes an optional post-op action as needed by the paymaster |


#### Public Mempool-recognized Validation Prefixes

The public mempool recognizes four validation prefixes. Structural rules are enforced only up to and including the frame that sets `payer`.

##### Self Relay

###### Basic Transaction

```
+-------------+
| self_verify |
+-------------+
```

###### Deploy New Account

```
+--------+-------------+
| deploy | self_verify |
+--------+-------------+
```

##### Canonical Paymaster

###### Basic Transaction

```
+-------------+-----+
| only_verify | pay |
+-------------+-----+
```

###### Deploy New Account

```
+--------+-------------+-----+
| deploy | only_verify | pay |
+--------+-------------+-----+
```

Frames after these prefixes are outside public mempool validation. For example, a transaction may continue with any number of `user_op`s and/or `post_op`s. There must not be any `VERIFY` frame after these prefixes, otherwise their revert would make entire transaction invalid.

#### Structural Rules

To be accepted into the public mempool, a frame transaction must satisfy the following:

1. Its validation prefix must match one of the four recognized prefixes above.
2. If present, `deploy` must be the first frame. This implies there can be at most one `deploy` frame in the validation prefix.
3. `self_verify` and `only_verify` must execute in `VERIFY` mode, target `tx.sender` (either explicitly or via a null target), must successfully call `APPROVE`, and `frame.flags` must match the scope of the `APPROVE` call.
    - `self_verify` must call `APPROVE(APPROVE_EXECUTION_AND_PAYMENT)`.
    - `only_verify` must call `APPROVE(APPROVE_EXECUTION)`.
4. `pay` must execute in `VERIFY` mode, have flags set to `APPROVE_PAYMENT`, and must successfully call `APPROVE(APPROVE_PAYMENT)`
5. No frame in the validation prefix may have the `ATOMIC_BATCH_FLAG` set.
6. The sum of `limits.execution` values across the validation prefix, plus the intrinsic cost of validating `tx.signatures`, must not exceed `MAX_VERIFY_GAS`. Additionally, the sum of `limits.state` values across the validation prefix must not exceed `MAX_VERIFY_STATE_GAS`. State gas does not measure node validation work — simulation work remains bounded by `MAX_VERIFY_GAS` alone — but the cap bounds the state growth admitted through the public mempool. A `deploy` frame may use state gas for account and code creation, and a `VERIFY` frame may use state gas only through `APPROVE` when incrementing the nonce creates the sender account.
7. Nodes should stop simulation immediately once `payer` has been set and the associated `VERIFY` frame completes successfully.
8. There must not be `VERIFY` frame after validation prefix.

#### Expiry Verifier Frame

Although `TIMESTAMP` is generally banned during validation-prefix execution, it is permitted when executing the canonical expiry verifier runtime code at `EXPIRY_VERIFIER`; clients may optimize the frame by performing the same deadline check without explicit EVM execution.

An `expiry_verify` frame MAY appear only as first frame in the frame list. For the purposes of matching the recognized validation-prefix shapes above, expiry verifier frame is skipped (e.g., `[expiry_verify, self_verify]` is recognized as `[self_verify]`).

A node MUST drop a frame transaction from the public mempool if it contains an `expiry_verify` frame whose deadline is less than the node's view of the current block timestamp at any point.

#### Canonical Paymaster Exception

The generic validation trace and opcode rules below apply to all frames in the validation prefix except a `pay` frame whose target runtime code exactly matches the canonical paymaster implementation. The canonical paymaster implementation is explicitly designed to be safe for public mempool use and is therefore admitted by code match, successful `APPROVE(APPROVE_PAYMENT)`, and the paymaster accounting rules in this section, rather than by requiring it to satisfy each generic validation rule individually.

#### Direct Evaluation of Protocol-Defined Frames

Three frame species in the validation prefix have fully protocol-defined semantics, leaving no deployed code whose behavior a node would need to discover by execution: a frame whose resolved target has the empty code hash (default code), an expiry verifier frame whose runtime code at `EXPIRY_VERIFIER` matches the canonical expiry verifier code, and a `pay` frame admitted by canonical paymaster code match per the previous section.

When every frame in the validation prefix is one of these, direct evaluation of the protocol-defined semantics is equivalent to simulation, and a node MAY use it to satisfy the validation requirements below. Direct evaluation MUST apply the same limits as simulation: signature validation and the evaluated frames' work count against `MAX_VERIFY_GAS`, and the paymaster accounting rules in this section apply unchanged.

The complete state dependency set of such a validation prefix is: the sender's code hash, nonce, and balance (the balance participates in the sender-existence check that decides the `APPROVE` account-creation state-gas charge), the payer's code hash and balance (or the canonical paymaster's tracked state), the runtime code at `EXPIRY_VERIFIER` together with the frame's deadline when an expiry verifier frame is present, and the current block timestamp. Nodes SHOULD index pending transactions by this set so that head-of-chain changes are revalidated without re-execution.

#### Validation Trace Rules

A public mempool node must simulate the validation prefix and reject the transaction if any of the following occurs before `payer` has been set:

- a frame in the validation prefix reverts
- a `self_verify`, `only_verify`, or `pay` frame exits without its required `APPROVE`
- total public-mempool validation work, including signature validation, exceeds `MAX_VERIFY_GAS`
- execution uses a banned opcode
- execution performs a state write, except inside the first `deploy` frame for (a) `CREATE`, `CREATE2`, or `SETDELEGATE` operations that install code at `tx.sender`, or (b) `SSTORE`s to `tx.sender`'s storage
- execution reads storage outside `tx.sender`
- execution performs `CALL*` or `EXTCODE*` to an address that is neither an existing contract nor a precompile, or to an address that uses an EIP-7702 delegation, except for `tx.sender` default-code behavior
- if a `deploy` frame is present, its execution does not result in non-empty code being installed at `tx.sender` (either conventional contract code or an EIP-7702 delegation indicator)

##### Banned Opcodes

For `VERIFY` frames, the usual `STATICCALL` restrictions apply except for the protocol-defined effects of `APPROVE`. In addition, the following opcodes are banned during the validation prefix, with a few caveats:

- GASPRICE (0x3A)
- BLOCKHASH (0x40)
- COINBASE (0x41)
- TIMESTAMP (0x42)
    - Except in an expiry verifier frame executing the canonical runtime code at `EXPIRY_VERIFIER`.
- NUMBER (0x43)
- PREVRANDAO/DIFFICULTY (0x44)
- GASLIMIT (0x45)
- BASEFEE (0x48)
- BLOBBASEFEE (0x4A)
- SLOTNUM (0x4B, [EIP-7843](./eip-7843.md))
- GAS (0x5A)
    - Except when followed immediately by a `*CALL` instruction. This is the standard method of passing gas to a child call and does not create an additional public mempool dependency.
- CREATE (0xF0)
    - Except inside the first `deploy` frame.
- CREATE2 (0xF5)
    - Except inside the first `deploy` frame.
- SETDELEGATE (0xF6, [EIP-7819](./eip-7819.md))
    - Except inside the first `deploy` frame.
- INVALID (0xFE)
- SELFDESTRUCT (0xFF)
- BALANCE (0x31)
- SELFBALANCE (0x47)
- SSTORE (0x55)
    - Except `SSTORE`s to `tx.sender`'s storage inside the first `deploy` frame.

`SLOAD` can be used only to access `tx.sender` storage, including when reached transitively via `CALL*` or `DELEGATECALL`.

`CALL*` and `EXTCODE*` may target any existing contract or precompile, provided the resulting trace still satisfies the storage, opcode, and EIP-7702 restrictions above. This permits helper contracts and libraries during validation, including via `DELEGATECALL`, so long as they do not introduce additional mutable-state dependencies.

#### Paymasters

A paymaster can choose to sponsor a transaction's gas. Generally the relationship is one paymaster to many transaction senders, however, this is in direct conflict with the goal of not predicating the validity of many transactions on the value of one account or storage element.

We address this conflict in two ways:

1. If a paymaster sponsors gas for a large number of accounts simultaneously, it must be a safe, standardized paymaster contract. It is designed such that ether which enters it cannot leave except:
  a. in the form of payment for a transaction, or
  b. after a delay period.
2. If a paymaster sponsors gas for a small number of accounts simultaneously (no more than `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER`), it may be any paymaster contract.

##### Canonical paymaster

The canonical paymaster is not a singleton deployment. Many instances may be deployed. For public mempool purposes, a paymaster instance is considered canonical if and only if the runtime code at the `pay` frame target exactly matches the canonical paymaster implementation.

The canonical paymaster in this draft authorizes with a single secp256k1 signer via `ecrecover`, does not support contract-signature schemes, and may change in later specifications, in which case a new canonical implementation version would be required.

Because the canonical paymaster implementation is explicitly standardized to be safe for public mempool use, nodes do not need to apply the generic validation trace and opcode rules to that `pay` frame. Instead, they identify it by runtime code match and apply the paymaster-specific accounting and revalidation rules in this section.

A transaction using a paymaster is eligible for public mempool propagation only if the `pay` frame targets a canonical paymaster instance and the node can reserve the maximum transaction cost against that paymaster.

For public mempool purposes, each node maintains a local accounting value `reserved_pending_cost(paymaster)` and computes:

```python
available_paymaster_balance = state.balance(paymaster) - reserved_pending_cost(paymaster) - pending_withdrawal_amount(paymaster)
```

Where `pending_withdrawal_amount(paymaster)` is the currently pending delayed withdrawal amount of the canonical paymaster instance, or zero if no delayed withdrawal is pending.

A node must reject a paymaster transaction if `available_paymaster_balance` is less than the transaction's maximum cost (`TXPARAM(0x06)`).

On admission, the node increments `reserved_pending_cost(paymaster)` by the transaction's maximum cost (`TXPARAM(0x06)`). On eviction, replacement, inclusion, or reorg removal, the node decrements it accordingly.

##### Non-canonical paymaster

For non-canonical paymasters, `pending_withdrawal_amount` is not meaningful since they may not support timelocked withdrawals.  Instead, we keep the mempool safe by enforcing that each non-canonical paymaster can only be used with no more than `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER` pending transactions.

Therefore we perform two checks:

- For balance, `available_paymaster_balance` must not be less than the transaction cost, where:

```python
available_paymaster_balance = state.balance(paymaster) - reserved_pending_cost(paymaster)
```

- The number of pending transactions in the mempool that uses this paymaster must be less than `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER`.

[See here for rationale](#non-canonical-paymasters-in-the-mempool) for enabling non-canonical paymasters in the mempool.

#### Acceptance Algorithm

1. A transaction is received over the wire and the node decides whether to accept or reject it.
2. The node validates all protocol-validated signatures and structurally checks all `ARBITRARY` signatures. If any signature is malformed or invalid, reject.
3. The node analyzes the frame structure and determines the validation prefix. If the prefix is not one of the recognized prefixes, reject.
4. The node simulates the validation prefix and enforces the structural and trace rules above, except that a `pay` frame whose target runtime code exactly matches the canonical paymaster implementation is handled via the canonical paymaster exception and the paymaster-specific rules below.
5. The node records the sender storage slots read during validation. Calls into helper contracts do not create additional mutable-state dependencies unless they cause disallowed storage access under the trace rules above.
6. If a canonical paymaster instance is used, the node verifies paymaster solvency using the reservation rule above.
7. A node should keep at most one pending frame transaction per sender in the public mempool. A new transaction from the same sender MAY replace the existing one only if it uses the same nonce and satisfies the replacement rules below.
8. If all checks pass, the transaction may be accepted into the public mempool and propagated to peers.

#### Replacement and Eviction

Pending frame transactions are identified by `(sender, nonce)`. The sender's nonce is consumed exactly once per transaction when payment is approved, so two pending transactions sharing it are alternatives, of which at most one can ever be included. Different variants of the same transaction share that identity and are therefore replacement candidates, not distinct transactions.

A replacement must be valid under all rules in this section and should only be accepted and propagated if it increases both `max_fee_per_gas` and `max_priority_fee_per_gas` by at least a node-configured minimum increment (10% is the conventional default). A blob-carrying frame transaction additionally follows the blob pool's replacement conventions for `max_fee_per_blob_gas`. A replacement may name a different payer than the transaction it replaces. The node should release the reservation or exposure held for the old payer and shift it to the new payer atomically with the replacement.

Reservation accounting applies to every payer, not only canonical paymasters. A node must not hold pending frame transactions whose summed maximum costs exceed the payer's balance, tracked per payer exactly as `reserved_pending_cost` is tracked for canonical paymaster instances. For canonical paymaster instances this is the reservation rule above. For any other payer it bounds aggregate exposure so that a single balance change cannot invalidate an unbounded set of pending transactions. The `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER` cap continues to apply to `pay` frames whose target carries code that is not the canonical paymaster implementation; a `pay` frame whose target has the empty code hash (a default-code sponsor) is not a paymaster and is governed by the per-payer exposure rule alone.

When local resource limits are reached, a node should evict in this order: first transactions already invalid against the current head, then transactions with the nearest expiry deadline, then transactions with the lowest effective priority fee. Evicted and replaced transactions must not be re-propagated. Re-admission requires receiving the transaction again. On any eviction or replacement the corresponding payer reservation or exposure is released.

#### Revalidation

When a new canonical block is accepted, the node removes any included frame transactions from the public mempool, updates paymaster reservations accordingly, and identifies the remaining pending transactions whose tracked dependencies were touched by the block. This includes at least transactions for the same sender, transactions whose recorded sender storage slots changed, transactions that reference a canonical paymaster instance whose balance, code, or delayed-withdrawal state changed, and transactions whose payer's balance or code changed. The node then re-simulates the validation prefix of only those affected transactions against the new head and evicts any transaction that no longer satisfies the public mempool rules.

#### Transaction origination

Do not apply the restriction put in place by [EIP-3607](./eip-3607.md) to frame transactions.
Specifically, `SENDER` frames originate calls where `tx.sender` is a contract account.
Validation logic for other transaction types remains unchanged, i.e. the transaction is only valid if the sender account's code is either empty or a valid delegation indicator.

### Networking

In the `Receipts` message of the protocol version carrying this fork, a frame transaction receipt is encoded mirroring its consensus `ReceiptPayload`:

```
receipt = [tx-type, cumulative-gas, payer, [[status, [execution-gas-used, state-gas-used], logs], ...]]
```

A frame transaction with non-empty `blob_versioned_hashes` is propagated with its blob sidecar exactly as an [EIP-4844](./eip-4844.md) blob transaction. During transaction gossip responses (`PooledTransactions`), its [EIP-2718](./eip-2718.md) `TransactionPayload` is wrapped per [EIP-7594](./eip-7594.md):

```
rlp([tx_payload_body, wrapper_version, blobs, commitments, cell_proofs])
```

The wrapper is validated with the same rules as EIP-7594: matching counts, `commitments` corresponding to `blob_versioned_hashes` via `kzg_to_versioned_hash`, and cell proof verification. Like blob transactions, blob-carrying frame transactions are not broadcast in full `Transactions` messages; they are announced via `NewPooledTransactionHashes` and requested on demand.

For body retrieval responses (`BlockBodies`), the plain `TransactionPayload` is used, as for blob transactions.

A frame transaction with empty `blob_versioned_hashes` uses the plain `TransactionPayload` with no wrapper.

## Rationale

### Canonical signature hash

The canonical signature hash is provided in `TXPARAM` to simplify the development of smart accounts.

Computing the signature hash in EVM is complicated and expensive. While using the canonical signature hash is not mandatory, it is strongly recommended. Creating a bespoke signature requires precise commitment to the underlying transaction data. Without this, it's possible that some elements can be manipulated in-the-air while the transaction is pending and have unexpected effects. This is known as transaction malleability. Using the canonical signature hash avoids malleability of the frames.

The raw `signature` bytes of signatures with empty `msg` are elided from the canonical signature hash. This is done for three reasons:

1. A signature over `compute_sig_hash(tx)` cannot commit to its own raw bytes.
1. Signatures with explicit 32-byte `msg` values do not induce this circularity, so their raw bytes are currently committed by the transaction signature hash.
1. In the future it may be desired to aggregate or otherwise externalize these signatures for data and compute efficiency reasons.

### `APPROVE` calling convention

Originally `APPROVE` was meant to extend the space of return statuses from 0 and 1 today to 0 to 4. However, this would mean smart accounts deployed today would not be able to modify their contract code to return with a different value at the top level. For this reason, we've chosen behavior above: `APPROVE` terminates the executing frame successfully like `RETURN`, but it actually updates the transaction scoped values `sender_approved` and `payer` during execution. It is still required that only the sender can toggle the `sender_approved` to `true`. Only the frame's resolved target can call `APPROVE` generally, because it can allow the transaction pool and other frames to better reason about `VERIFY` mode frames.

Because `DELEGATECALL` preserves `ADDRESS`, code executed via `DELEGATECALL` from the resolved target may also execute `APPROVE` successfully. Contracts that rely on `APPROVE` should therefore treat delegatecalled libraries as fully trusted.

### Payer in receipt

The payer cannot be determined statically from a frame transaction and is relevant to users. The only way to provide this information safely and efficiently over the JSON-RPC is to record this data in the receipt object.

### No authorization list

The EIP-7702 authorization list heavily relies on ECDSA cryptography to determine the authority of accounts to delegate code. While delegations could be used in other manners later, it does not satisfy the PQ goals of the frame transaction.

### No access list

The access list was introduced to address a particular backwards compatibility issue that was caused by [EIP-2929](./eip-2929.md). The risk-reward of using an access list successfully is high. A single miss, paying to warm a storage slot that does not end up getting used, causes the overall transaction cost to be greater than had it not been included at all.

Future optimizations based on pre-announcing state elements a transaction will touch will be covered by block level access lists.

### Atomic batching

Atomic batching allows multiple frames to be grouped into a single all-or-nothing unit. This is useful when a sequence of calls is only meaningful if all succeed together, such as an approval followed by a swap, or a series of interdependent state changes. Without this feature, a revert in one frame would leave the preceding frames' state changes applied, potentially leaving the account in an undesirable intermediate state.

Using a flag to indicate atomic batches saves us from having to introduce a new mode. Batches are identified purely by consecutive frames with the flag set, terminated by a frame without it. This design enables consecutive atomic batches since the batch boundary is clearly indicated by the frame without the flag.

Approval scope flags are disallowed on the frames of an atomic batch, including its terminating frame. Since `APPROVE` requires its scope to be present in `frame.flags`, this restriction is statically checkable, and it keeps the approval context constant across a batch. Unrolling a failed batch therefore never rolls back the sender nonce increment or the `max_cost` collection, a batch failure cannot retroactively withdraw an execution approval that later `SENDER` frames rely on, and whether the transaction sets `payer` never depends on a batch outcome. Approval can always be placed in a frame preceding the batch instead.

### Per-frame cost

Each frame incurs a fixed CALL execution-context overhead (100) plus `G_log` (375) for the receipt sub-entry it produces, giving `FRAME_TX_PER_FRAME_COST = 475`. The execution-context component covers context setup, mode dispatch, and the two-pool gas accounting at the frame boundary, analogous to the fixed overhead of a CALL. The `G_log` component covers the `[status, gas_used, logs]` receipt sub-entry that each frame adds to the transaction receipt, which must be serialized, hashed into the receipt trie, and proven by ZK-EVM implementations. Cold/warm access costs for the frame's target account are charged within the frame's own `limits.execution` through the normal EVM warm/cold accounting, not through the per-frame cost.

### Frame entry access charge

The resolved target's warm/cold access is charged at frame entry, before the balance check and before dispatch, because resolving the target's code — the read the charge prices — is what dispatch is. Charging at entry makes the cost uniform across dispatch outcomes: a frame pays the same access whether its target carries contract code, an [EIP-7702](./eip-7702.md) delegation, or no code at all. This follows the same principle as the expiry verifier frame, which consumes gas according to normal EVM execution rules even when clients evaluate it directly: protocol-defined evaluation is an optimization, never a pricing change. It also keeps sponsorship neutral: a code-less payer approving through the default code and a contract payer approving through its own code price identically, and the payer needs no separate warming rule, since the payer is always the executing frame's resolved target. The intrinsic cost's payer-settlement component covers the settlement balance writes — the escrow and the refund — not the payer's first account touch, which is this charge.

### Per-frame value

A design goal of the frame transaction is to provide a good experience out-of-the-box for users and to reduce the threat surface of smart contract wallets. Like batching, sending native value is a part of achieving that.

Restricting non-zero `value` to `SENDER` frames keeps `VERIFY` and `DEFAULT` frames side-effect-free with respect to ETH transfer semantics, preserves the intended `STATICCALL`-like behavior of `VERIFY`, and avoids requiring the protocol-defined `ENTRY_POINT` caller to fund top-level ETH transfers.

### Two-dimensional gas budgets

[EIP-8037](./eip-8037.md) meters execution gas and state gas separately, but its transactions carry a single gas field. The split must therefore be derived at runtime. `gas_left` is capped at the [EIP-7825](./eip-7825.md) limit and the excess becomes a reservoir spendable only on state gas. Under this model the sender cannot bound either dimension individually, the split does not exist below the cap, and a block builder must reserve the transaction's full gas in both dimensions since either could consume it.

The frame transaction is a new envelope, so the split is explicitly declared. Each frame's `limits` list carries a `state` budget next to its `execution` budget, and the two pools never mix. This avoids the reservoir accounting entirely, keeps the EIP-7825 check static, lets the mempool bound the validation prefix per dimension without simulation, and makes block reservations exact. A deploy frame that needs tens of millions of state gas for a code deposit just declares it.

Budgets are per-frame rather than per-transaction for the same reason unused gas does not roll over between frames. Frames belong to mutually distrusting parties. With a shared pool, a user operation could drain the state gas a paymaster's `post_op` frame depends on. [ERC-4337](./eip-4337.md) works around this by reserving `postOp` gas in the `EntryPoint`; here the reservation is part of the transaction. Since `GAS` never reports state gas, `FRAMEPARAM` and `TXPARAM` expose state budgets and usage directly, allowing a paymaster to check budgets before approving payment and attribute usage afterwards.

### Calldata floor and payment

The EIP-7623 calldata floor participates in block accounting per dimension, exactly as under EIP-8037: the floor is compared against the transaction's execution component alone, because calldata is an execution-dimension resource and the bytes-per-block bound must be a property of that dimension's counter. Comparing against the transaction total would let state-gas padding carry calldata into the execution dimension below the floor rate.

The payment rule departs from EIP-8037. EIP-8037 retrofits two-dimensional accounting onto existing transaction types and therefore keeps EIP-7623's transaction-total floor, accepting that a transaction's block-accounted dimensions can sum to more than it pays; state gas may absorb into floor headroom just as execution gas does today. The frame transaction is a new envelope with no such compatibility constraint, so it defines `gas_used = tx_execution_gas + tx_state_gas`. The payer buys exactly the capacity the transaction denies other transactions in each dimension, state growth never rides free under the data floor, and receipt `cumulative_gas_used` deltas reconcile with block accounting. The stricter rule costs only transactions that are simultaneously floor-bound and state-growing, which pay `calldata_floor_gas + tx_state_gas` where an EIP-8037 transaction with the identical workload pays its transaction-total floor.

### Cross-frame state-gas refills

EIP-8037's refill conditions are transaction-scoped and so a slot's *original value* is its value at transaction start. Only one refill can cross a frame boundary, an `SSTORE` clearing a slot that an earlier frame created. `SELFDESTRUCT` carries no refill under EIP-8037, even for contracts created earlier in the transaction ([EIP-6780](./eip-6780.md)), and account creation and code-deposit charges are only undone by rolling back the frame that made them.

Crediting a cross-frame refill to the executing frame would hand it budget it never declared. Dropping the refill would overcharge common patterns like approve-and-swap and cause `block_state_gas_used` to overstate real state growth, which the `CPSB` derivation depends on. Reducing the owning frame's `gas_used.state` avoids both. The payer is made whole at settlement, receipts sum to the transaction's net state gas, and no frame gains spendable budget. Since receipt mutations are journaled with the state changes that cause them, atomic-batch unrolls reverse them automatically.

### Intrinsic cost decomposition

`FRAME_TX_INTRINSIC_COST` is [EIP-2780](./eip-2780.md)'s `TX_BASE_COST`, which prices one ECDSA recovery, the sender's account access and write, and inclusion of the transaction's bytes in a block. Frame transactions price signature recovery per `tx.signatures` entry, so the recovery component instead covers payer settlement, which standard transactions do not price separately. Like EIP-2780 and [EIP-8038](./eip-8038.md), these values are provisional benchmark targets.

Value-bearing frames are charged `TX_VALUE_COST` for the recipient balance write and transfer log, as EIP-2780 charges top-level transfers. The charge is static because `value` and `target` are transaction fields. Frame targets do not pay EIP-2780's unconditional cold-rate recipient touch. Frames behave like internal calls, which stay on the [EIP-2929](./eip-2929.md) warm/cold model, and a transaction repeatedly targeting the same account would otherwise overpay.

EIP-2780's split between intrinsic and runtime gas carries over. Intrinsic gas is state-independent and decides validity. Every state-dependent charge, including frame-entry account creation and sender creation by `APPROVE`, is metered inside a frame's `limits.state`. Exhausting it halts the current call frame. After approval the transaction is still included and pays for the gas consumed, while a failed `VERIFY` frame invalidates the transaction as usual.

### Transaction execution gas cap

[EIP-7825](./eip-7825.md) caps the transaction's full execution budget: `frame_tx_intrinsic_gas` plus the sum of all `limits.execution` values must not exceed `TX_MAX_GAS_LIMIT`, and the calldata floor is checked against the same cap. State gas is excluded, bounded only by the encoding limit and block state-gas capacity, matching EIP-8037.

### Public Key Aliases

Future signature schemes with large public keys may benefit from a state-backed alias mechanism. Such an alias could be a 20-byte address that identifies a public key stored in state, allowing transactions to reference the address instead of carrying the full public key each time.

A future extension could represent the alias account as non-executable code containing a canonical public key object, for example:

```text
0xef02 || version || key_type || pubkey_len || pubkey
```

where `key_type` defines the cryptosystem, public key encoding, valid lengths, and validation rules for `pubkey`.

That extension could also define a `PUBLISHPK` instruction to validate a public key, wrap it in the canonical alias-code format, derive the alias address, and install the alias code.

### Externally Owned Account (EOA) support

While we expect EOA users to migrate to smart accounts eventually, we recognize that most Ethereum users today are using EOAs, so we want to improve UX for them where we can.

Thanks to the default code, EOAs today can use frame transactions to reap many benefits of account abstraction, including sending sponsored transactions, paying gas in [ERC-20](./eip-20.md) tokens, batch transactions, and more.

### Non-canonical paymasters in the mempool

The primary use case for non-canonical paymasters is to enable users to pay gas with a dedicated "gas account," so that their other accounts can transact without holding any ETH. For example, a user might have a single account that holds some ETH, while other accounts only hold stablecoins and NFTs, and they can transact freely with these other accounts while using the gas account as the paymaster.

Note that users can use any EOA as a paymaster thanks to the [default code](#default-code).

### Examples

#### Example 1: Simple Transaction

| Frame | Mode   | Caller      | Flags                         | Target        | Value | Data      |
| ----- | ------ | ----------- | ----------------------------- | ------------- | ----- | --------- |
| 0     | VERIFY | ENTRY_POINT | APPROVE_EXECUTION_AND_PAYMENT | Null (sender) | 0     | Empty     |
| 1     | SENDER | Sender      | APPROVE_SCOPE_NONE            | Target        | 0     | Call data |

Frame 0 uses a signature entry with empty `msg` for the sender and calls `APPROVE(APPROVE_EXECUTION_AND_PAYMENT)` to approve both payment and execution. Frame 1 executes and exits normally via `RETURN`.

The mempool can process this transaction with the following static validation and call:

- Verify that the first frame is a `VERIFY` frame.
- Verify that the call of frame 0 succeeds, and does not violate the mempool rules (similar to [ERC-7562](./eip-7562.md)).

#### Example 1a: Simple ETH transfer

| Frame | Mode   | Caller      | Flags                         | Target        | Value  | Data      |
| ----- | ------ | ----------- | ----------------------------- | ------------- | ------ | --------- |
| 0     | VERIFY | ENTRY_POINT | APPROVE_EXECUTION_AND_PAYMENT | Null (sender) | 0      | Empty     |
| 1     | SENDER | Sender      | APPROVE_SCOPE_NONE            | Destination   | Amount | Empty     |

A simple transfer is performed by setting the `SENDER` frame target to the destination account and the frame `value` to the transfer amount. This requires two frames for mempool compatibility, since the validation phase of the transaction has to be static.

#### Example 1b: Simple account deployment

| Frame | Mode    | Caller      | Flags                         | Target        | Value  | Data           |
| ----- | ------- | ----------- | ----------------------------- | ------------- | ------ | -------------- |
| 0     | DEFAULT | ENTRY_POINT | APPROVE_SCOPE_NONE            | Deployer      | 0      | Initcode, Salt |
| 1     | VERIFY  | ENTRY_POINT | APPROVE_EXECUTION_AND_PAYMENT | Null (sender) | 0      | Empty          |
| 2     | SENDER  | Sender      | APPROVE_SCOPE_NONE            | Destination   | Amount | Empty          |

This example illustrates the initial deployment flow for a smart account at the `sender` address. Since the address needs to have code in order to validate the transaction, the transaction must deploy the code before verification.

The first frame would call the [EIP-7997](./eip-7997.md) deterministic factory predeploy. The deployer determines the address in a deterministic way from the salt and initcode. However, since the transaction sender is not authenticated at this point, the user must choose an initcode which is safe to deploy by anyone.

#### Example 2: Atomic Approve + Swap

| Frame | Mode   | Caller      | Flags                         | Target        | Value | Data                 |
| ----- | ------ | ----------- | ----------------------------- | ------------- | ----- | -------------------- |
| 0     | VERIFY | ENTRY_POINT | APPROVE_EXECUTION_AND_PAYMENT | Null (sender) | 0     | Empty                |
| 1     | SENDER | Sender      | ATOMIC_BATCH_FLAG             | ERC-20        | 0     | approve(DEX, amount) |
| 2     | SENDER | Sender      | APPROVE_SCOPE_NONE            | DEX           | 0     | swap(...)            |

Frame 0 uses a signature entry with empty `msg` and calls `APPROVE(APPROVE_EXECUTION_AND_PAYMENT)`. Frames 1 and 2 form an atomic batch: if the swap in frame 2 reverts, the ERC-20 approval from frame 1 is also reverted, preventing the account from being left with a dangling approval.

#### Example 3: Sponsored Transaction (Fee Payment in ERC-20)

| Frame | Mode    | Caller      | Flags              | Target        | Value | Data                   |
| ----- | ------- | ----------- | ------------------ | ------------- | ----- | ---------------------- |
| 0     | VERIFY  | ENTRY_POINT | APPROVE_EXECUTION  | Null (sender) | 0     | Empty                  |
| 1     | VERIFY  | ENTRY_POINT | APPROVE_PAYMENT    | Sponsor       | 0     | Sponsor data           |
| 2     | SENDER  | Sender      | APPROVE_SCOPE_NONE | ERC-20        | 0     | transfer(Sponsor,fees) |
| 3     | SENDER  | Sender      | APPROVE_SCOPE_NONE | Target addr   | 0     | Call data              |
| 4     | DEFAULT | ENTRY_POINT | APPROVE_SCOPE_NONE | Sponsor       | 0     | Post op call           |

- Frame 0: Uses the sender's signature entry with empty `msg` and calls `APPROVE(APPROVE_EXECUTION)` to authorize execution from sender.
- Frame 1: Inspects signature metadata in `tx.signatures` if needed, and checks that the next frame is an ERC-20 send of the right size to the sponsor. Calls `APPROVE(APPROVE_PAYMENT)` to authorize payment.
- Frame 2: Sends tokens to sponsor.
- Frame 3: User's intended call.
- Frame 4 (optional): Check unpaid gas, refund tokens, possibly convert tokens to ETH on an AMM.

Note: to be included in the public mempool under the current model, sponsors must accept some risk of frontrunning by the sponsee who can zero out their ERC-20 balance before the sponsored transaction lands on-chain.

### Data Efficiency

**Basic transaction sending ETH from a smart account:**

| Field                             | Bytes |
| --------------------------------- | ----- |
| Tx wrapper                        | 1     |
| Chain ID                          | 1     |
| Nonce                             | 2     |
| Sender                            | 20    |
| Max priority fee                  | 5     |
| Max fee                           | 5     |
| Max fee per blob gas              | 1     |
| Blob versioned hashes (empty)     | 1     |
| Signatures wrapper                | 1     |
| Sender tx signature: scheme       | 1     |
| Sender tx signature: signer       | 1     |
| Sender tx signature: msg          | 0     |
| Sender tx signature: signature    | 65    |
| Frames wrapper                    | 1     |
| Sender validation frame: mode     | 1     |
| Sender validation frame: flags    | 1     |
| Sender validation frame: target   | 1     |
| Sender validation frame: execution gas      | 2     |
| Sender validation frame: state gas | 1    |
| Sender validation frame: value    | 1     |
| Sender validation frame: data     | 0     |
| Execution frame: mode             | 1     |
| Execution frame: flags            | 1     |
| Execution frame: target           | 20    |
| Execution frame: execution gas              | 1     |
| Execution frame: state gas        | 3     |
| Execution frame: value            | 5     |
| Execution frame: data             | 0     |
| **Total**                         | 141   |

Notes: Nonce assumes < 65536 prior sends. Fees assume < 1099 gwei. Validation frame target is 1 byte because target is `tx.sender`. Validation gas assumes <= 65,536 gas. Validation frame value is zero. Execution frame target is encoded directly as the destination address. Execution frame value assumes a compact 5-byte encoding. The execution frame data is empty for a plain ETH transfer. The signature is a secp256k1 entry with empty `msg` using a 65-byte ECDSA signature. Blob fields assume no blobs (empty list, zero max fee). State gas limits are zero, assuming the destination exists; a transfer that creates a new account instead requires a `STATE_BYTES_PER_NEW_ACCOUNT × CPSB = 183,600` state gas budget on the execution frame (three additional bytes).

This is not much larger than an EIP-1559 transaction; the extra overhead is mainly the need to specify the sender and the per-frame wrapper explicitly.

**First transaction from an account (add deployment frame):**

| Field                      | Bytes |
| -------------------------- | ----- |
| Deployment frame: mode     | 1     |
| Deployment frame: flags    | 1     |
| Deployment frame: target   | 20    |
| Deployment frame: execution gas      | 3     |
| Deployment frame: state gas | 3    |
| Deployment frame: value    | 1     |
| Deployment frame: data     | 100   |
| **Total additional**       | 129   |

Notes: Gas assumes cost < 2^24. State gas covers account creation plus a proxy-sized code deposit (< 2^24). Calldata assumes small proxy.

**Trustless pay-with-ERC-20 sponsor (add these frames):**

| Field                                | Bytes |
| ------------------------------------ | ----- |
| Sponsor validation frame: mode       | 1     |
| Sponsor validation frame: flags      | 1     |
| Sponsor validation frame: target     | 20    |
| Sponsor validation frame: execution gas        | 3     |
| Sponsor validation frame: state gas  | 1     |
| Sponsor validation frame: value      | 1     |
| Sponsor validation frame: calldata   | 0     |
| Send to sponsor frame: mode          | 1     |
| Send to sponsor frame: flags         | 1     |
| Send to sponsor frame: target        | 20    |
| Send to sponsor frame: execution gas           | 3     |
| Send to sponsor frame: state gas     | 1     |
| Send to sponsor frame: value         | 1     |
| Send to sponsor frame: calldata      | 68    |
| Sponsor post op frame: mode          | 2     |
| Sponsor post op frame: flags         | 1     |
| Sponsor post op frame: target        | 20    |
| Sponsor post op frame: execution gas           | 3     |
| Sponsor post op frame: state gas     | 1     |
| Sponsor post op frame: value         | 1     |
| Sponsor post op frame: calldata      | 0     |
| **Total additional**                 | 150   |

Notes: Sponsor can read info from other fields. ERC-20 transfer call is 68 bytes. State gas fields assume no new storage slots are created; an ERC-20 transfer into a fresh balance slot requires a 97,920 state gas budget (three additional bytes).

There is some inefficiency in the sponsor case, because the same sponsor address must appear in three places (sponsor validation, send to sponsor inside ERC-20 calldata, post op frame), and the ABI is inefficient (~12 + 24 bytes wasted on zeroes). This is difficult to mitigate in a "clean" way, because one of the duplicates is inside the ERC-20 call, "opaque" to the protocol. However, it is much less inefficient than [ERC-4337](./eip-4337.md), because not all of the data takes the hit of the 32-byte-per-field ABI overhead.


### Blob support

Blobs are optional for frame transactions because `FRAME_TX_TYPE` is a general-purpose transaction type, unlike [EIP-4844](./eip-4844.md) transactions which exist only to carry blobs. The networking wrapper is reused from [EIP-7594](./eip-7594.md) unchanged so that existing blob pool handling and future data availability sampling changes apply to frame transactions uniformly.

## Backwards Compatibility

The `ORIGIN` opcode behavior changes for frame transactions, returning the frame's caller rather than the traditional transaction origin. This is consistent with the precedent set by EIP-7702, which already modified `ORIGIN` semantics. Contracts that rely on `ORIGIN = CALLER` for security checks (a discouraged pattern) may behave differently under frame transactions.

Wallets and gas estimators must produce two-dimensional, per-frame gas estimates: each frame's `limits.execution` covers its execution gas and its `limits.state` covers its state gas, and neither budget can borrow from the other or from other frames. Under-provisioned state gas halts a frame exactly like out-of-gas; over-provisioned budgets are refunded at settlement but raise the up-front `max_cost` collected from the payer. When the calldata floor binds, `gas_used` is `calldata_floor_gas + tx_state_gas`, so fee estimation for data-heavy transactions must add the state dimension on top of the floor rather than assuming EIP-7623's transaction-total floor.

## Security Considerations

### Transaction Propagation

Frame transactions introduce new denial-of-service vectors for transaction pools that node operators must mitigate. Because validation logic is arbitrary EVM code, attackers can craft transactions that appear valid during initial validation but become invalid later. Without any additional policies, an attacker could submit many transactions whose validity depends on some shared state, then submit one transaction that modifies that state, and cause all other transactions to become invalid simultaneously. This wastes the computational resources nodes spent validating and storing these transactions.

#### Example Attack

A simple example is transactions that check `block.timestamp`, without using expiry verifier frame:

```solidity
function validateTransaction() external {
    require(block.timestamp < SOME_DEADLINE, "expired");
    // ... rest of validation
    APPROVE(APPROVE_EXECUTION_AND_PAYMENT);
}
```

Such transactions are valid when submitted but become invalid once the deadline passes, without any on-chain action required from the attacker.

#### Deploy Frame Front-Running

If a transaction uses a `deploy` frame, that frame executes before the sender is authenticated. An observer can front-run the same deterministic deployment and cause the `deploy` frame to fail because code is already present at `tx.sender`. Accordingly, the deploy frame's calldata (and any initcode it carries) must be safe to submit by any party, and wallets should expect resubmission without the `deploy` frame once deployment has already occurred.

#### Explicit Sender State-Read Amplification

Because `tx.sender` is explicit in the transaction envelope, an attacker can submit many invalid frame transactions that name arbitrary sender addresses and force nodes to read sender state, including the nonce check required before execution. Public mempool implementations should therefore perform all available structural and stateless checks before sender-state access and should consider peer-level rate limiting or other DoS mitigations for repeated invalid transactions that vary `tx.sender`.

#### Cross-frame Data Visibility During Validation

`FRAMEPARAM`, `FRAMEDATALOAD`, and `FRAMEDATACOPY` allow validation code to inspect other frames, including later `SENDER` frames and their `value`s. As a result, paymasters and other `VERIFY` frames can observe user operation parameters before approval and may condition their behavior on that information. Users should therefore treat non-`VERIFY` frame parameters and data as visible to validation logic and should not rely on untrusted paymasters or verifiers to keep such information private.

#### Arbitrary Signature Malleability

The protocol performs no cryptographic validation for `ARBITRARY` signatures. When an `ARBITRARY` signature has empty `msg`, its raw bytes are elided from the canonical signature hash and can therefore be changed without changing `TXPARAM(0x08)`. If the custom verifier accepts multiple encodings for the same witness the transaction hash may be malleable even though the canonical signature hash is not. Custom verifiers should enforce canonical encodings and reject unused bytes. When an `ARBITRARY` signature uses an explicit 32-byte `msg`, its raw bytes are committed to by the canonical signature hash and this particular source of transaction-hash malleability does not apply.

##### Mitigations

Node implementations should consider restricting which opcodes and storage slots validation frames can access, similar to ERC-7562. This isolates transactions from each other and limits mass invalidation vectors.

It's recommended that to *validate* the transaction, a specific frame structure is enforced and the amount of gas that is expended executing the validation phase must be limited. Once the validation prefix reaches payer approval via `APPROVE(APPROVE_PAYMENT)` or `APPROVE(APPROVE_EXECUTION_AND_PAYMENT)`, the transaction can be included in the mempool and propagated to peers safely.

For deployment of the sender account in the first frame, the mempool enforces deploy-frame determinism via the validation trace rules. Any contract may be used as `frame.target`, provided the frame's execution satisfies those rules — notably, it must not read mutable state outside `tx.sender` or maintain per-deploy factory storage (counters, reentrancy flags, etc.), so that the deployment result is independent of chain state. Factories deployed via the [EIP-7997](./eip-7997.md) deterministic factory predeploy are the canonical choice for acquiring a cross-chain-stable factory address.

In general, it can be assumed that handling of frame transactions imposes similar restrictions as EIP-7702 on mempool relay, i.e. only a single transaction can be pending for an account that uses frame transactions.

### State Gas Isolation Between Frames

Frame state-gas budgets are deliberately not shared. A shared, transaction-scoped state budget would allow one frame to exhaust the state gas a later frame depends on: for example, a user operation could drain the budget a paymaster's `post_op` frame needs to write its accounting slot, causing it to halt. Because each frame's `limits.state` is declared in the transaction and unavailable to other frames, validation code and paymasters can verify via `FRAMEPARAM`, before approving, that every frame they depend on carries the state budget it needs. The cross-frame refill rules preserve this isolation: clearing state paid for by an earlier frame reduces that earlier frame's `gas_used.state` and credits the payer at settlement, but never increases the executing frame's spendable budget.

### Execution Approval Authorizes All Subsequent Sender Frames

`sender_approved` is a single transaction-scoped flag. Once a frame grants `APPROVE_EXECUTION` (or `APPROVE_EXECUTION_AND_PAYMENT`), every subsequent `SENDER` frame executes with `caller` set to `tx.sender`, not only the frame the approving code inspected. The approval is not scoped to a particular frame or call target.

Validation code must therefore bind its approval to the entire set of frames it is willing to authorize. A signature with an empty `msg` commits to this, because it is verified over `compute_sig_hash(tx)`, which covers the full frame list; the default validation code relies on exactly such a signature. A validator that instead approves on the basis of an explicit 32-byte `msg` signature, or any check that does not commit to the frames, authorizes an open-ended list. Because a frame transaction carries no outer signature over the whole envelope, an observer can then reuse that approval with a different set of `SENDER` frames, since those frames were never part of what the validator verified. This is the "unchecked fields" class of issue familiar from account-abstraction relaying, now expressed in-protocol.

Custom validation contracts that grant `APPROVE_EXECUTION` should verify against the canonical signature hash, or otherwise constrain every subsequent `SENDER` frame, before approving.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
