EIP-8141 - Frame Transaction

Created 2026-01-29
Status Draft
Category Core
Type Standards Track
Authors
Requires

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
ENTRY_POINT address(0xaa)
MAX_FRAMES 10^3

Opcodes

Name Value
APPROVE 0xaa
TXPARAM 0xb0
FRAMEDATALOAD 0xb1
FRAMEDATACOPY 0xb2

New Transaction Type

A new EIP-2718 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, target, gas_limit, data], ...]

If no blobs are included, blob_versioned_hashes must be an empty list and max_fee_per_blob_gas must be 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.

The execution mode of a frame is identified by the lower bits (<= 8) of the mode field. The modes are explained in detail below.

mode & 0xFF 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, state cannot be modified.

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 and can only be used after explicitly approved.

Mode Flags

The upper bits (> 8) of mode configure the execution environment.

Mode bit Meaning Valid with
9-10 Approval scope Any mode
11 Atomic batch SENDER 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.

Constraints

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

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.frames[n].mode & 0xFF)  < 3
assert len(tx.frames[n].target) == 20 or tx.frames[n].target is None

# Atomic batch flag (bit 11) is only valid with SENDER mode, and next frame must also be SENDER.
for i, frame in enumerate(tx.frames):
    if (frame.mode >> 10) & 1 == 1:
        assert (frame.mode & 0xFF) == 2               # must be SENDER
        assert i + 1 < len(tx.frames)                  # must not be last frame
        assert (tx.frames[i + 1].mode & 0xFF) == 2     # 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:

def compute_sig_hash(tx: FrameTx) -> Hash:
    for i, frame in enumerate(tx.frames):
        if (frame.mode & 0xFF) == VERIFY:
            tx.frames[i].data = Bytes()
    return keccak(rlp(tx))

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 frame.target (i.e. if ADDRESS != frame.target), APPROVE reverts.

Stack
Stack Value
top - 0 offset
top - 1 length
top - 2 scope
Scope Operand

The scope operand must be one of the following values:

  1. 0x1: Approval of execution - the sender contract approves future frames calling on its behalf.
  2. Note this is only valid when frame.target equals tx.sender.
  3. 0x2: Approval of payment - the contract approves paying the total gas cost for the transaction.
  4. 0x3: Approval of execution and payment - combines both 0x1 and 0x2.

Any other value results in an exceptional halt.

Usable scope operands are constrained by bits 9 and 10 of the frame.mode, and using a non-allowed scope also results in an exceptional halt.

Behavior

The behavior of APPROVE is defined as follows:

TXPARAM opcode

This opcode gives access to information from the transaction header and/or frames. The gas cost of this operation is 2.

It takes two values from the stack, param and in2 (in this order). The param is the field to be extracted from the transaction. in2 names a frame index.

param in2 Return value
0x00 must be 0 current transaction type
0x01 must be 0 nonce
0x02 must be 0 sender
0x03 must be 0 max_priority_fee_per_gas
0x04 must be 0 max_fee_per_gas
0x05 must be 0 max_fee_per_blob_gas
0x06 must be 0 max cost (basefee=max, all gas used, includes blob cost and intrinsic cost)
0x07 must be 0 len(blob_versioned_hashes)
0x08 must be 0 compute_sig_hash(tx)
0x09 must be 0 len(frames) (can be zero)
0x10 must be 0 currently executing frame index
0x11 frame index target
0x12 frame index gas_limit
0x13 frame index mode (the lower 8 bits of frame.mode)
0x14 frame index len(data)
0x15 frame index status (exceptional halt if current/future)
0x16 frame index scope (bits 9/10 from frame.mode)
0x17 frame index atomic_batch (bit 11 from frame.mode, returns 0 or 1)

Notes:

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.The gas cost matches CALLDATACOPY, i.e. the operation has a fixed cost of 3 and a variable cost that accounts for the memory expansion and copying.

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:

Initialize with transaction-scoped variables:

Then for each call frame:

  1. Execute a call with the specified mode, target, gas_limit, and data.
  2. If target is null, set the call target to tx.sender.
  3. If mode is SENDER:
    • sender_approved must be true. If not, the transaction is invalid.
    • Set caller as tx.sender.
  4. If mode is DEFAULT or VERIFY:
    • Set the caller to ENTRY_POINT.
  5. If frame.target has no code, execute the logic described in default code.
  6. The ORIGIN opcode returns frame caller throughout all call depths.
  7. 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.
  8. 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 11) 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:
  4. Restore the state to the snapshot taken before the batch.
  5. 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:

Default code

When using frame transactions with EOAs (accounts with no code), they are treated as if they have a "default code." 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.

Notes:

Here's the logic above implemented in Python:

DEFAULT   = 0
VERIFY    = 1
SENDER    = 2

SECP256K1 = 0x0
P256      = 0x1

def default_code(frame, tx):
    mode = frame.mode & 0xFF                 # equivalent to TXPARAMLOAD(0x14, TXPARAMLOAD(0x10))

    if mode == VERIFY:
        scope          = (frame.mode >> 8) & 3     # approval scope from mode bits
        if scope == 0:
            revert()
        signature_type = frame.data[0]              # first byte: signature type
        sig_hash       = compute_sig_hash(tx)       # equivalent to TXPARAMLOAD(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]
            if frame.target != ecrecover(sig_hash, v, r, s):
                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 frame.target != keccak256(qx + qy)[12:]:
                revert()
            if not P256VERIFY(sig_hash, r, s, qx, qy):
                revert()

        else:
            revert()

        APPROVE(scope)

    elif mode == SENDER:
        if frame.target != tx.sender:
            revert()

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

Gas Accounting

The total gas limit of the transaction is:

tx_gas_limit = FRAME_TX_INTRINSIC_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.

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(0x2) or APPROVE(0x3)) 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, 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. a known deterministic deployer contract, 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 a known deterministic deployer
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_ops and/or post_ops.

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(0x2).
    • only_verify must call APPROVE(0x0).
  4. pay must execute in VERIFY mode and successfully call APPROVE(0x1).
  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(0x1), 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:

Banned Opcodes

The following opcodes are banned during the validation prefix, with a few caveats:

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.

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:

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, 0)).

On admission, the node increments reserved_pending_cost(paymaster) by the transaction's maximum cost (TXPARAM(0x06, 0)). 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:

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

See here for rationale 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 TXPARAMLOAD 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 two 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 frame.target of VERIFY frames is covered by the signature hash, i.e. the sender chooses the sponsor address explicitly.

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.target can call APPROVE generally, because it can allow the transaction pool and other frames to better reason about VERIFY mode frames.

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.

No value in frame

It is not required because the account code can send value.

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.

Examples

Example 1: Simple Transaction

Frame Caller Target Data Mode
0 ENTRY_POINT Null (sender) Signature VERIFY
1 Sender Target Call data SENDER

Frame 0 verifies the signature and calls APPROVE(0x3) to approve both execution and payment. Frame 1 executes and exits normally via RETURN.

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

Example 1a: Simple ETH transfer

Frame Caller Target Data Mode
0 ENTRY_POINT Null (sender) Signature VERIFY
1 Sender Null (sender) Destination/Amount SENDER

A simple transfer is performed by instructing the account to send ETH to the destination account. This requires two frames for mempool compatibility, since the validation phase of the transaction has to be static.

This is listed here to illustrate why the transaction type has no built-in value field.

Example 1b: Simple account deployment

Frame Caller Target Data Mode
0 ENTRY_POINT Deployer Initcode, Salt DEFAULT
1 ENTRY_POINT Null (sender) Signature VERIFY
2 Sender Null (sender) Destination/Amount 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 a deployer contract, like EIP-7997. The deployer determines the address in a deterministic way, such as by hashing the initcode and salt. 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 Data Mode Atomic Batch
0 ENTRY_POINT Null (sender) Signature VERIFY -
1 Sender ERC-20 approve(DEX, amount) SENDER set
2 Sender DEX swap(...) SENDER not set

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

Example 4: EOA paying gas in ERC-20s

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

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: target 1
Sender validation frame: gas 2
Sender validation frame: data 65
Sender validation frame: mode 1
Execution frame: target 1
Execution frame: gas 1
Execution frame: data 20+5
Execution frame: mode 1
Total 134

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. Calldata is 65 bytes for 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 the need to specify the sender and amount in calldata explicitly.

First transaction from an account (add deployment frame):

Field Bytes
Deployment frame: target 20
Deployment frame: gas 3
Deployment frame: data 100
Deployment frame: mode 1
Total additional 124

Notes: Gas assumes cost < 2^24. Calldata assumes small proxy.

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

Field Bytes
Sponsor validation frame: target 20
Sponsor validation frame: gas 3
Sponsor validation frame: calldata 0
Sponsor validation frame: mode 1
Send to sponsor frame: target 20
Send to sponsor frame: gas 3
Send to sponsor frame: calldata 68
Send to sponsor frame: mode 1
Sponsor post op frame: target 20
Sponsor post op frame: gas 3
Sponsor post op frame: calldata 0
Sponsor post op frame: mode 2
Total additional 141

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:

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

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

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 frame calls APPROVE(0x2), it 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 specific and known deployer factory contracts to be used 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.