---
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)
discussions-to: https://ethereum-magicians.org/t/frame-transaction/27617
status: Draft
type: Standards Track
category: Core
created: 2026-01-29
requires: 1559, 2718, 4844, 7997
---

## Abstract

Add a new transaction whose validity and gas payment can be defined abstractly. Instead of relying solely on a single ECDSA signature, accounts may freely define and interpret their signature scheme using any cryptographic system.

## Motivation

This new transaction provides a native off-ramp from the elliptic curve based cryptographic system used to authenticate transactions today, to post-quantum (PQ) secure systems.

In doing so, it realizes the original vision of account abstraction: unlinking accounts from a prescribed ECDSA key and support alternative fee payment schemes. The assumption of 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` | `15000`         |
| `FRAME_TX_PER_FRAME_COST` | `475`           |
| `ENTRY_POINT`             | `address(0xaa)` |
| `MAX_FRAMES`              | `64`            |

`ENTRY_POINT` is a protocol-defined distinguished caller address used for `DEFAULT` and `VERIFY` frames. It is not specified as a deployed contract or precompile, and contracts MUST NOT assume anything about its code, balance, or caller type beyond address equality. Contracts called from `DEFAULT` or `VERIFY` frames therefore observe `CALLER = ENTRY_POINT` and `CALLVALUE = 0`, so heuristics that infer EOA-or-contract properties from `CALLER` may not behave as expected. Its numeric equality with the `APPROVE` opcode value has no semantic significance.

### Opcodes

| Name            | Value  |
|-----------------|--------|
| `APPROVE`       | `0xaa` |
| `TXPARAM`       | `0xb0` |
| `FRAMEDATALOAD` | `0xb1` |
| `FRAMEDATACOPY` | `0xb2` |
| `FRAMEPARAM`    | `0xb3` |

### New Transaction Type

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".

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

```
[chain_id, nonce, sender, frames, max_priority_fee_per_gas, max_fee_per_gas, max_fee_per_blob_gas, blob_versioned_hashes]

frames = [[mode, flags, target, gas_limit, value, data], ...]
```

If no blobs are included, `blob_versioned_hashes` must be an empty list and `max_fee_per_blob_gas` must be `0`.

Frame transactions do not include an [EIP-7702](./eip-7702.md) authorization list and do not set, clear, or otherwise modify EIP-7702 delegation indicators.

For execution semantics, each frame has a **resolved target**:

```python
resolved_target = frame.target if frame.target is not None else tx.sender
```

Unless otherwise stated, checks below that refer to the target account during execution use the resolved target.

Each frame also has a `value` field, interpreted as the top-level call value in wei. A non-zero `value` is valid only in `SENDER` mode; `DEFAULT` and `VERIFY` frames must set `value = 0`.

#### Frame Modes

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`                  |
| 3..255  | reserved       |

##### `DEFAULT` Mode

Frame executes as regular call where the caller address is `ENTRY_POINT`.

##### `VERIFY` Mode

Identifies the frame as a validation frame. Its purpose is to *verify* that a sender and/or payer authorized the transaction. It must call `APPROVE` during execution. Failure to do so will result in the whole transaction being invalid.

The execution behaves the same as `STATICCALL` for user code: state cannot otherwise be modified. The `APPROVE` opcode is the only exception and applies its protocol-defined effects, including approval updates and, for payment scopes, nonce increment and gas-charge collection.

Frames in this mode will have their data elided from signature hash calculation and from introspection by other frames.

##### `SENDER` Mode

Frame executes as regular call where the caller address is `sender`. This mode effectively acts on behalf of the transaction sender, can only be used after explicitly approved, and is the only mode that may send a non-zero `value`.

##### Frame Flags

The `flags` field configures additional execution constraints.

Bit positions in this specification are zero-based, with the least significant bit numbered `0`.

| Flag bits | Meaning                      | Valid with  |
|-----------|------------------------------| ----------- |
| 0-1       | Approval scope               | Any mode    |
| 2         | Atomic batch                 | SENDER mode |
| 3-7       | reserved, must be zero       |             |

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.

#### Constraints

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

```python
# Constants (see Default Code section for full definitions)
VERIFY = 1
SENDER = 2
APPROVE_SCOPE_MASK = 0x03
ATOMIC_BATCH_FLAG = 0x04

assert tx.chain_id < 2**256
assert tx.nonce < 2**64
assert len(tx.frames) > 0 and len(tx.frames) <= MAX_FRAMES
assert len(tx.sender) == 20
assert tx.sender != bytes(20)
total_frame_gas = 0
for frame in tx.frames:
    assert frame.mode < 3
    assert frame.flags < 8
    assert frame.mode != VERIFY or (frame.flags & APPROVE_SCOPE_MASK) != 0  # VERIFY frames must permit a non-zero APPROVE scope
    assert frame.target is None or len(frame.target) == 20
    assert frame.gas_limit <= 2**63 - 1
    assert frame.value < 2**256
    assert frame.mode == SENDER or frame.value == 0
    total_frame_gas += frame.gas_limit
    assert total_frame_gas <= 2**63 - 1

# Atomic batch flag (bit 2 of flags) is only valid with SENDER mode, and next frame must also be SENDER.
for i, frame in enumerate(tx.frames):
    if frame.flags & ATOMIC_BATCH_FLAG:
        assert frame.mode == SENDER                    # must be SENDER
        assert i + 1 < len(tx.frames)                  # must not be last frame
        assert tx.frames[i + 1].mode == SENDER         # next frame must be SENDER
```

#### Receipt

The `ReceiptPayload` is defined as:

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

`payer` is the address of the account that paid the fees for the transaction. `status` is the return code of the top-level call.

#### Signature Hash

With the frame transaction, the signature may be at an arbitrary location in the frame list. In the canonical signature hash any frame with mode `VERIFY` will have its data elided:

```python
def compute_sig_hash(tx: FrameTx) -> Hash:
    # Operate on a copy; the original transaction object is not modified.
    tx_copy = deep_copy(tx)
    for i, frame in enumerate(tx_copy.frames):
        if frame.mode == VERIFY:
            tx_copy.frames[i].data = Bytes()
    return keccak(rlp(tx_copy))
```

### New Opcodes

#### `APPROVE` opcode (`0xaa`)

The `APPROVE` opcode is like `RETURN (0xf3)`. It exits the current context successfully and updates the transaction-scoped approval context based on the `scope` operand.

If the currently executing account is not the frame's resolved target (i.e. if `ADDRESS != resolved_target`), `APPROVE` reverts.

##### Stack

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

##### Scope Operand

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

1. `APPROVE_SCOPE_NONE = 0x0`
2. `APPROVE_PAYMENT = 0x1`
3. `APPROVE_EXECUTION = 0x2`
4. `APPROVE_PAYMENT_AND_EXECUTION = APPROVE_PAYMENT | APPROVE_EXECUTION`
5. `APPROVE_SCOPE_MASK = APPROVE_PAYMENT_AND_EXECUTION`

The `scope` operand must be a non-zero subset of `APPROVE_SCOPE_MASK`, i.e. one of the following values:

1. `APPROVE_PAYMENT` (`0x1`): Approval of payment - the contract approves paying the total gas cost for the transaction.
2. `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`.
3. `APPROVE_PAYMENT_AND_EXECUTION` (`0x3`): Approval of payment and execution.

Any other value, including `APPROVE_SCOPE_NONE`, results in an exceptional halt.

`APPROVE_PAYMENT_AND_EXECUTION` is processed atomically within a single `APPROVE` and is not equivalent to invoking `APPROVE` twice.

The frame's allowed approval scope is `allowed_scope = frame.flags & APPROVE_SCOPE_MASK`. These flag bits use the same bit assignments as the `scope` operand. A `scope` that is not a non-zero subset of `allowed_scope` results in an exceptional halt.

`allowed_scope` is caller-supplied policy input and may restrict a verifier's intended `APPROVE` call. Verification logic SHOULD authenticate any approval scope it relies on and MUST NOT treat `allowed_scope` as trusted unless it is covered by that logic.

- If `allowed_scope == APPROVE_SCOPE_NONE`, no `APPROVE` scope is allowed.
- If `allowed_scope == APPROVE_EXECUTION`, only `APPROVE_EXECUTION` is allowed.
- If `allowed_scope == APPROVE_PAYMENT`, only `APPROVE_PAYMENT` is allowed.
- If `allowed_scope == APPROVE_PAYMENT_AND_EXECUTION`, `APPROVE_EXECUTION`, `APPROVE_PAYMENT`, or `APPROVE_PAYMENT_AND_EXECUTION` are allowed.

##### Behavior

The behavior of `APPROVE` is defined as follows:

- If `ADDRESS != resolved_target`, revert.
- For scopes `APPROVE_EXECUTION`, `APPROVE_PAYMENT`, and `APPROVE_PAYMENT_AND_EXECUTION`, execute the following:
    - `APPROVE_EXECUTION`: Set `sender_approved = true`.
        - If `sender_approved` was already set, revert the frame.
        - If `resolved_target` != `tx.sender`, revert the frame.
    - `APPROVE_PAYMENT`: Increment the sender's nonce, collect the transaction's maximum cost (`TXPARAM(0x06)`) from `resolved_target`, and set `payer_approved = true`.
        - If `payer_approved` was already set, revert the frame.
        - If `resolved_target` has insufficient balance, revert the frame.
        - If `sender_approved == false`, revert the frame.
    - `APPROVE_PAYMENT_AND_EXECUTION`: Set `sender_approved = true`, increment the sender's nonce, collect the transaction's maximum cost (`TXPARAM(0x06)`) from `resolved_target`, and set `payer_approved = true`.
        - If `sender_approved` or `payer_approved` was already set, revert the frame.
        - If `resolved_target` != `tx.sender`, revert the frame.
        - If `resolved_target` has insufficient balance, revert the frame.

#### `TXPARAM` opcode

This opcode gives access to transaction-scoped information. The gas
cost of this operation is `2`.

It takes one value from the stack, `param`. The `param` is the field to be extracted from the transaction.

| `param` | Return value                                                                |
|---------|-----------------------------------------------------------------------------|
| 0x00    | current transaction type                                                    |
| 0x01    | `nonce`                                                                     |
| 0x02    | `sender`                                                                    |
| 0x03    | `max_priority_fee_per_gas`                                                  |
| 0x04    | `max_fee_per_gas`                                                           |
| 0x05    | `max_fee_per_blob_gas`                                                      |
| 0x06    | max cost (basefee=max, all gas used, includes blob cost and intrinsic cost) |
| 0x07    | `len(blob_versioned_hashes)`                                                |
| 0x08    | `compute_sig_hash(tx)`                                                      |
| 0x09    | `len(frames)`                                                               |
| 0x0A    | currently executing frame index                                             |

Notes:

- `0x01` has a possible future extension to allow indices for multidimensional nonces.
- `0x03` and `0x04` have a possible future extension to allow indices for multidimensional gas.
- `0x06` covers only the maximum gas and blob fees. It does not include any `frame.value` transfers.
- `0x07` returns only the number of blob versioned hashes. Individual blob versioned hashes remain accessible via the existing `BLOBHASH(index)` opcode.
- `0x08` returns the canonical signature hash. This value MUST be computed at most once per transaction and cached.
- Invalid `param` values (not defined in the table above) result in an exceptional halt.

#### `FRAMEPARAM` opcode

This opcode gives access to frame-scoped information. The gas cost of this operation is `2`.

It takes two values from the stack, `param` and `frameIndex` (in this order). The `frameIndex` is zero-based, so `0` refers to the first frame.

| `param` | `frameIndex` | Return value                                              |
|---------|--------------|-----------------------------------------------------------|
| 0x00    | frameIndex   | `target`                                                  |
| 0x01    | frameIndex   | `gas_limit`                                               |
| 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`                                                   |

Notes:

- The `status` field (0x05) returns `0` for failure or `1` for success.
- Invalid `param` values (not defined in the table above) result in an exceptional halt.
- Out-of-bounds access for `frameIndex` (`>= len(frames)`) results in an exceptional halt.
- Attempting to access the return `status` of the current frame or a subsequent frame results in an exceptional halt.
- `len(data)` returns size 0 when called on a frame with `VERIFY` set.

#### `FRAMEDATALOAD` opcode

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

It takes two values from the stack, an `offset` and `frameIndex`.
It places the retrieved data on the stack.

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

The operation sematics match CALLDATALOAD, returning a word of data from the chosen
frame's `data`, starting at the given byte `offset`. When targeting a frame in `VERIFY`
mode, the returned data is always zero.

#### `FRAMEDATACOPY` opcode

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: `memOffset`, `dataOffset`, `length` and `frameIndex`.
No stack output value is produced.

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

The operation sematics match CALLDATACOPY, copying `length` bytes from the chosen frame's
`data`, starting at the given byte `dataOffset`, into a memory region starting at
`memOffset`. When targeting a frame in `VERIFY` mode, no data is copied.

### Behavior

When processing a frame transaction, perform the following steps.

Perform stateful validation check:

- Ensure `tx.nonce == state[tx.sender].nonce`

Initialize with transaction-scoped variables:

- `payer_approved = false`
- `sender_approved = false`

Then for each call frame:

1. Let `resolved_target = frame.target if frame.target is not null else tx.sender`, then execute a call with the specified `mode`, `flags`, `resolved_target`, `gas_limit`, `value`, and `data`.
   - If mode is `SENDER`:
       - `sender_approved` must be `true`. If not, the transaction is invalid.
       - Set `caller` as `tx.sender`.
   - If mode is `DEFAULT` or `VERIFY`:
       - Set the `caller` to `ENTRY_POINT`.
   - 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.
   - If `resolved_target` has neither code nor an [EIP-7702](./eip-7702.md) delegation indicator, 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.
   - The `ORIGIN` opcode returns frame `caller` throughout all call depths.
   - If a frame's execution reverts, its state changes are discarded. Additionally, if this frame has the atomic batch flag set, mark all subsequent frames in the same atomic group as skipped.
2. If frame has mode `VERIFY` and the frame did not successfully call `APPROVE`, the transaction is invalid.

#### Atomic Batching

Consecutive `SENDER` frames where all but the last have the atomic batch flag (bit 2 of `flags`) set form an **atomic batch**. Within a batch, if any frame reverts, all preceding frames in the batch are also reverted and all subsequent frames in the batch are skipped.

More precisely, execution of an atomic batch proceeds as follows:

1. Take a snapshot of the state before executing the first frame in the batch.
2. Execute each frame in the batch sequentially.
3. If a frame reverts:
   - Restore the state to the snapshot taken before the batch.
   - Mark all remaining frames in the batch as skipped.

For example, given frames:

| Frame | Mode   | Atomic Batch Flag |
|-------|--------|-------------------|
| 0     | SENDER | set               |
| 1     | SENDER | not set           |
| 2     | SENDER | set               |
| 3     | SENDER | set               |
| 4     | SENDER | not set           |

Frames 0-1 form one atomic batch and frames 2-4 form another. If frame 3 reverts, the state changes from frames 2 and 3 are discarded and frame 4 is skipped.

After executing all frames, verify that `payer_approved == true`. If it is, refund any unpaid gas to the gas payer. If it is not, the whole transaction is invalid.

Note:

- It is implied by the handling that the sender must approve the transaction *before* the payer and that once `sender_approved` or `payer_approved` become `true` they cannot be re-approved or reverted.

#### Default code

When using frame transactions with EOAs that have neither code nor an [EIP-7702](./eip-7702.md) delegation indicator, they are treated as if they have a "default code." Accounts with code, including [EIP-7702](./eip-7702.md) delegated accounts, do not use the default code path. This spec describes only the behavior of the default code; clients are free to implement the default code however they want, so long as they correspond to the behavior specified here.

- Let `resolved_target = frame.target if frame.target is not null else tx.sender`.
- Retrieve the current frame's `mode` with `FRAMEPARAM`.
- 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.
  - If `allowed_scope & APPROVE_EXECUTION != 0` and `resolved_target != tx.sender`, revert.
  - Read the first byte of `frame.data` as `signature_type`.
  - If `signature_type` is:
    - `0x0`:
      - Read the rest of `frame.data` as `(v, r, s)`.
      - If `s > secp256k1n / 2`, revert.
      - Let `recovered = ecrecover(sig_hash, v, r, s)`, where `sig_hash = compute_sig_hash(tx)`.
      - If `recovered == address(0)`, revert.
      - If `resolved_target != recovered`, revert.
    - `0x1`:
      - Read the rest of `frame.data` as `(r, s, qx, qy)`.
      - Let `p256_address = keccak(P256_ADDRESS_DOMAIN|qx|qy)[12:]`.
      - If `resolved_target != p256_address`, revert.
      - If `P256VERIFY(sig_hash, r, s, qx, qy) != true`, where `sig_hash = compute_sig_hash(tx)`, revert.
    - Otherwise revert.
  - Call `APPROVE(allowed_scope)`.
- If `mode` is `SENDER`:
  - If `resolved_target != tx.sender`, return successfully with empty data. This matches a call to an empty-code account; any top-level `frame.value` transfer has already been applied by the frame call itself.
  - Otherwise, read `frame.data` as RLP encoding of `calls = [[target, value, data]]`.
  - For each call in `calls`, execute the call with `msg.sender = tx.sender`.
    - If any call reverts, revert the frame.
- If `mode` is `DEFAULT`:
  - Revert the frame.

Notes:

- `P256VERIFY` must reject invalid public keys, including points that are not on the P256 curve.
- For the P256 (r1) signature type, the sender address is `keccak(P256_ADDRESS_DOMAIN|qx|qy)[12:]`.

Here's the logic above implemented in Python:

```python
DEFAULT   = 0
VERIFY    = 1
SENDER    = 2
APPROVE_SCOPE_NONE = 0x00
APPROVE_PAYMENT = 0x01
APPROVE_EXECUTION = 0x02
APPROVE_PAYMENT_AND_EXECUTION = APPROVE_PAYMENT | APPROVE_EXECUTION
APPROVE_SCOPE_MASK = APPROVE_PAYMENT_AND_EXECUTION
ATOMIC_BATCH_FLAG = 0x04

SECP256K1 = 0x0
P256      = 0x1
P256_ADDRESS_DOMAIN = b"\x01"
SECP256K1N_DIV_2 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0

def default_code(frame, tx):
    mode = frame.mode                        # equivalent to FRAMEPARAM(0x02, TXPARAM(0x0A))
    resolved_target = frame.target if frame.target is not None else tx.sender

    if mode == VERIFY:
        allowed_scope  = frame.flags & APPROVE_SCOPE_MASK  # allowed approval scope from flags field
        if allowed_scope == APPROVE_SCOPE_NONE:
            revert()
        if allowed_scope & APPROVE_EXECUTION != 0 and resolved_target != tx.sender:
            revert()
        signature_type = frame.data[0]              # first byte: signature type
        sig_hash       = compute_sig_hash(tx)       # equivalent to TXPARAM(0x08)

        if signature_type == SECP256K1:
            # frame.data layout: [signature_type, v (1 byte), r (32 bytes), s (32 bytes)]
            if len(frame.data) != 66:               # 1 header + 65 signature bytes
                revert()
            v = frame.data[1]
            r = frame.data[2:34]
            s = frame.data[34:66]
            # Reject high-s signatures so each authorization has a single canonical encoding.
            if int.from_bytes(s, "big") > SECP256K1N_DIV_2:
                revert()
            recovered = ecrecover(sig_hash, v, r, s)
            if recovered == bytes(20):
                revert()
            if resolved_target != recovered:
                revert()

        elif signature_type == P256:
            # frame.data layout: [signature_type, r (32 bytes), s (32 bytes), qx (32 bytes), qy (32 bytes)]
            if len(frame.data) != 129:              # 1 header + 128 signature bytes
                revert()
            r  = frame.data[1:33]
            s  = frame.data[33:65]
            qx = frame.data[65:97]
            qy = frame.data[97:129]
            if resolved_target != keccak256(P256_ADDRESS_DOMAIN + qx + qy)[12:]:
                revert()
            if not P256VERIFY(sig_hash, r, s, qx, qy):
                revert()

        else:
            revert()

        APPROVE(allowed_scope)

    elif mode == SENDER:
        if resolved_target != tx.sender:
            # Empty-code account behavior: succeed immediately. Any top-level
            # frame.value transfer has already been applied by the frame call.
            return

        # frame.data layout: RLP-encoded [[target, value, data], ...]
        calls = rlp_decode(frame.data)
        for call_target, call_value, call_data in calls:
            result = evm_call(caller=tx.sender, to=call_target, value=call_value, data=call_data)
            if result.reverted:
                revert()

    elif mode == DEFAULT:
        revert()

    else:
        revert()
```

#### Frame interactions

A few cross-frame interactions to note:

- For the purposes of gas accounting of warm / cold state status, the journal of such touches is shared across frames.
- Discard the `TSTORE` and `TLOAD` transient storage between frames.

#### Gas Accounting

The total gas limit of the transaction is:

```
tx_gas_limit = FRAME_TX_INTRINSIC_COST + len(tx.frames) * FRAME_TX_PER_FRAME_COST + calldata_cost(rlp(tx.frames)) + sum(frame.gas_limit for all frames)
```

Where `calldata_cost` is calculated per standard EVM rules (4 gas per zero byte, 16 gas per non-zero byte).

The total fee is defined as:

```
tx_fee = tx_gas_limit * effective_gas_price + blob_fees
blob_fees = len(blob_versioned_hashes) * GAS_PER_BLOB * blob_base_fee
```

The `effective_gas_price` is calculated per EIP-1559 and `blob_fees` is calculated as per EIP-4844.

Any `frame.value` transferred by `SENDER` frames is separate from `tx_fee` and follows ordinary `CALL` value-transfer semantics. The gas cost of sending `frame.value` is the same as for any ordinary `CALL` frame.

Each frame has its own `gas_limit` allocation. Unused gas from a frame is **not** available to subsequent frames. After all frames execute, the gas refund is calculated as:

```
refund = sum(frame.gas_limit for all frames) - total_gas_used
```

This refund is returned to the gas payer (the `target` that called `APPROVE(APPROVE_PAYMENT)` or `APPROVE(APPROVE_PAYMENT_AND_EXECUTION)`) and added back to the block gas pool. *Note: This refund mechanism is separate from EIP-3529 storage refunds.*

### 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 simulating 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_approved = true`.

Public mempool rules apply only to the validation prefix. Once `payer_approved = true`, 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.

#### 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 sender's nonce, code, and storage,
3. the [EIP-7997](./eip-7997.md) deterministic factory predeploy, if a deployment frame is present,
4. 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,
5. 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  | Description  |
|---|---|---|
| `self_verify`   | VERIFY  | Validates the transaction and approves both sender and payer |
| `deploy`  | DEFAULT | Deploys a new smart account using the [EIP-7997](./eip-7997.md) deterministic factory predeploy |
| `only_verify`  | VERIFY  | Validates the transaction and approves only the sender |
| `pay`  | VERIFY | Validates the transaction and approves only the payer |
| `user_op` | SENDER | Executes the intended user operation |
| `post_op` | DEFAULT | 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_approved = true`.

##### 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.

#### 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), and must successfully call `APPROVE`.
    - `self_verify` must call `APPROVE(APPROVE_PAYMENT_AND_EXECUTION)`.
    - `only_verify` must call `APPROVE(APPROVE_EXECUTION)`.
4. `pay` must execute in `VERIFY` mode and successfully call `APPROVE(APPROVE_PAYMENT)`.
5. The sum of `gas_limit` values across the validation prefix must not exceed `MAX_VERIFY_GAS`.
6. Nodes should stop simulation immediately once `payer_approved = true` has been observed.

#### 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.

#### Validation Trace Rules

A public mempool node must simulate the validation prefix and reject the transaction if any of the following occurs before `payer_approved = true`:

- a frame in the validation prefix reverts
- a `VERIFY` frame in the validation prefix exits without the required `APPROVE`
- execution exceeds `MAX_VERIFY_GAS`
- execution uses a banned opcode
- execution performs a state write, except deterministic deployment performed by the first `deploy` frame through a known deployer
- 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, non-delegated code being installed at `tx.sender`

##### 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:

- ORIGIN (0x32)
- GASPRICE (0x3A)
- BLOCKHASH (0x40)
- COINBASE (0x41)
- TIMESTAMP (0x42)
- NUMBER (0x43)
- PREVRANDAO/DIFFICULTY (0x44)
- GASLIMIT (0x45)
- BASEFEE (0x48)
- BLOBHASH (0x49)
- BLOBBASEFEE (0x4A)
- 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)
- CREATE2 (0xF5)
    - Except inside the first `deploy` frame when targeting the [EIP-7997](./eip-7997.md) deterministic factory predeploy.
- INVALID (0xFE)
- SELFDESTRUCT (0xFF)
- BALANCE (0x31)
- SELFBALANCE (0x47)
- SSTORE (0x55)
- TLOAD (0x5C)
- TSTORE (0x5D)

`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 analyzes the frame structure and determines the validation prefix. If the prefix is not one of the recognized prefixes, reject.
3. 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.
4. 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.
5. If a canonical paymaster instance is used, the node verifies paymaster solvency using the reservation rule above.
6. 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 node's fee bump rules.
7. If all checks pass, the transaction may be accepted into the public mempool and propagated to peers.

#### 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, and transactions that reference a canonical paymaster instance whose balance, code, or delayed-withdrawal state 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.

## 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 other than `VERIFY`.

The `frame.data` of `VERIFY` frames is elided from the signature hash. This is done for three reasons:

1. It contains the signature so by definition it cannot be part of the signature hash.
2. In the future it may be desired to aggregate the cryptographic operations for data and compute efficiency reasons. If the data was introspectable, it would not be possible to aggregate the verify frames in the future.
3. For gas sponsoring workflows, we also recommend using a `VERIFY` frame to approve the gas payment. Here, the input data to the sponsor is intentionally left malleable so it can be added onto the transaction after the `sender` has made its signature. Notably, the raw `frame.target` field of `VERIFY` frames is covered by the signature hash, i.e. the `sender` chooses the sponsor address explicitly.

Implementations MUST NOT treat `VERIFY` frame `data` as sender-authenticated by the canonical signature hash. Any verifier or paymaster that depends on its input data, including sponsor parameters, exchange rates, fee terms, or custom account context, MUST authenticate that data independently. Replacing or mutating `VERIFY` frame `data` does not change the canonical signature hash.

By contrast, non-`VERIFY` frame metadata, including a `SENDER` frame's `value`, remains covered by the canonical signature hash.

Verification logic MUST NOT assign policy meaning to signature encodings or adjacent `VERIFY` frame data unless that meaning is independently authenticated.

### `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_approved` 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. 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 `SENDER` 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 `SENDER` frames with the flag set, terminated by a `SENDER` frame without it. This design enables consecutive atomic batches since the batch boundary is clearly indicated by the `SENDER` frame without the flag.

### 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 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 `gas_limit` through the normal EVM warm/cold accounting, not through the per-frame cost.

### 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.

### 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.

With frame transactions, EOA wallets today can reap the key benefit of AA - gas abstraction, including sending sponsored transactions, paying gas in ERC-20 tokens, 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 | Caller      | Target        | Value | Flags                         | Data      | Mode   |
| ----- | ----------- | ------------- | ----- | ----------------------------- | --------- | ------ |
| 0     | ENTRY_POINT | Null (sender) | 0     | APPROVE_PAYMENT_AND_EXECUTION | Signature | VERIFY |
| 1     | Sender      | Target        | 0     | APPROVE_SCOPE_NONE            | Call data | SENDER |

Frame 0 verifies the signature and calls `APPROVE(APPROVE_PAYMENT_AND_EXECUTION)` 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 | Caller      | Target        | Value  | Flags                         | Data      | Mode   |
| ----- | ----------- | ------------- | ------ | ----------------------------- | --------- | ------ |
| 0     | ENTRY_POINT | Null (sender) | 0      | APPROVE_PAYMENT_AND_EXECUTION | Signature | VERIFY |
| 1     | Sender      | Destination   | Amount | APPROVE_SCOPE_NONE            | Empty     | SENDER |

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 | Caller      | Target        | Value  | Flags                         | Data           | Mode    |
| ----- | ----------- | ------------- | ------ | ----------------------------- | -------------- | ------- |
| 0     | ENTRY_POINT | Deployer      | 0      | APPROVE_SCOPE_NONE            | Initcode, Salt | DEFAULT |
| 1     | ENTRY_POINT | Null (sender) | 0      | APPROVE_PAYMENT_AND_EXECUTION | Signature      | VERIFY  |
| 2     | Sender      | Destination   | Amount | APPROVE_SCOPE_NONE            | Empty          | SENDER  |

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 | Caller      | Target        | Value | Flags                         | Data                 | Mode   |
| ----- | ----------- | ------------- | ----- | ----------------------------- | -------------------- | ------ |
| 0     | ENTRY_POINT | Null (sender) | 0     | APPROVE_PAYMENT_AND_EXECUTION | Signature            | VERIFY |
| 1     | Sender      | ERC-20        | 0     | ATOMIC_BATCH_FLAG            | approve(DEX, amount) | SENDER |
| 2     | Sender      | DEX           | 0     | APPROVE_SCOPE_NONE           | swap(...)            | SENDER |

Frame 0 verifies the signature and calls `APPROVE(APPROVE_PAYMENT_AND_EXECUTION)`. 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 | Caller      | Target        | Value | Flags              | Data                   | Mode    |
| ----- | ----------- | ------------- | ----- | ------------------ | ---------------------- | ------- |
| 0     | ENTRY_POINT | Null (sender) | 0     | APPROVE_EXECUTION  | Signature              | VERIFY  |
| 1     | ENTRY_POINT | Sponsor       | 0     | APPROVE_PAYMENT    | Sponsor data           | VERIFY  |
| 2     | Sender      | ERC-20        | 0     | APPROVE_SCOPE_NONE | transfer(Sponsor,fees) | SENDER  |
| 3     | Sender      | Target addr   | 0     | APPROVE_SCOPE_NONE | Call data              | SENDER  |
| 4     | ENTRY_POINT | Sponsor       | 0     | APPROVE_SCOPE_NONE | Post op call           | DEFAULT |

- Frame 0: Verifies signature and calls `APPROVE(APPROVE_EXECUTION)` to authorize execution from sender.
- Frame 1: Checks that the user has enough ERC-20 tokens, and 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.

#### Example 4: EOA paying gas in ERC-20s

| Frame | Caller      | Target       | Value | Flags              | Data                   | Mode   |
| ----- | ----------- | ------------ | ----- | ------------------ | ---------------------- | ------ |
| 0     | ENTRY_POINT | Null(sender) | 0     | APPROVE_EXECUTION  | (0, v, r, s)           | VERIFY |
| 1     | ENTRY_POINT | Sponsor      | 0     | APPROVE_PAYMENT    | Sponsor signature      | VERIFY |
| 2     | Sender      | ERC-20       | 0     | APPROVE_SCOPE_NONE | transfer(Sponsor,fees) | SENDER |
| 3     | Sender      | Target addr  | 0     | APPROVE_SCOPE_NONE | Call data              | SENDER |

- Frame 0: Verify the sender with a EOA signature.  Upon verification, the frame calls `APPROVE(APPROVE_EXECUTION)` to authorize execution.
- Frame 1: Checks that the user has enough ERC-20 tokens, and 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.

### 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     |
| Frames wrapper                    | 1     |
| Sender validation frame: mode     | 1     |
| Sender validation frame: flags    | 1     |
| Sender validation frame: target   | 1     |
| Sender validation frame: gas      | 2     |
| Sender validation frame: value    | 1     |
| Sender validation frame: data     | 65    |
| Execution frame: mode             | 1     |
| Execution frame: flags            | 1     |
| Execution frame: target           | 20    |
| Execution frame: gas              | 1     |
| Execution frame: value            | 5     |
| Execution frame: data             | 0     |
| **Total**                         | 136   |

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. Validation data is 65 bytes for an ECDSA signature. Blob fields assume no blobs (empty list, zero max fee).

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: gas      | 3     |
| Deployment frame: value    | 1     |
| Deployment frame: data     | 100   |
| **Total additional**       | 126   |

Notes: Gas assumes cost < 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: gas        | 3     |
| 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: gas           | 3     |
| 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: gas           | 3     |
| Sponsor post op frame: value         | 1     |
| Sponsor post op frame: calldata      | 0     |
| **Total additional**                 | 147   |

Notes: Sponsor can read info from other fields. ERC-20 transfer call is 68 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, because not all of the data takes the hit of the 32-byte-per-field ABI overhead.


## 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.

## 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`:

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

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, initcode used with `deploy` frames must be safe to deploy 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.

##### 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_PAYMENT_AND_EXECUTION)`, 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 must only allow the [EIP-7997](./eip-7997.md) deterministic factory predeploy as `frame.target`, to ensure deployment is deterministic and independent of chain state.

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.

## Copyright

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