Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-7701: Native Account Abstraction

Native Account Abstraction protocol, relying on a new transaction type and a family of opcodes

Authors Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn)
Created 2024-05-01
Discussion Link https://ethereum-magicians.org/t/eip-7701-native-account-abstraction/19893

Abstract

We propose splitting the Ethereum transaction scope into multiple steps: validations, execution, and post-operation logic. Transaction validity is determined by the result of the validation steps of a transaction.

We further separate transaction validation for the purposes of authorization and the gas fee payment, allowing one contract to pay gas for a transaction that will be executed from another contract.

Motivation

Native Account Abstraction allows custom validation logic of a transaction and custom gas payment logic, opening new use-cases and features for wallets and dApps.

Specification

Constants

Name Value
AA_TX_TYPE TBD
AA_ENTRY_POINT address(0x7701)
AA_BASE_GAS_COST 15000

New Transaction Type

A new EIP-2718 transaction with type AA_TX_TYPE is introduced. Transactions of this type are referred to as “AA transactions”.

Their payload should be interpreted as:


AA_TX_TYPE || rlp([
  chain_id,
  nonce,
  sender, sender_validation_data,
  deployer, deployer_data,
  paymaster, paymaster_data,
  sender_execution_data,
  valid_after, valid_until,
  max_priority_fee_per_gas, max_fee_per_gas,
  sender_validation_gas_limit, paymaster_validation_gas_limit,
  sender_execution_gas_limit, paymaster_post_op_gas_limit,
  access_list,
  authorization_list
])

Definitions

  • Smart Contract Account: an Ethereum smart contract that serves as the user’s account and on-chain identity. It is responsible for holding user’s assets, verifying user requests, and executing actions on the user’s behalf.
  • Sender: the Smart Contract Account sending the current AA transaction.
  • Paymaster: a smart contract that is requested to pay gas fees for the current AA transaction on behalf of the Sender contract.
  • Factory: a smart contract that performs a deployment for a new Sender contract if necessary in the context of the current AA transaction.
  • Entity: a common term for any of the smart contracts mentioned above in the context of an EIP-7701 Transaction.
  • Transaction Validity: A property of an Ethereum transaction that describes whether this transaction can be included in a block without a violation of the ethereum execution and consensus rules. This property depends on both the inputs of the transaction and the current state of the Ethereum blockchain and can change over time.
  • EIP-7701 Transaction: the entire transaction initiated by the Sender Smart Contract Account and represented with an EIP-2718 compatible Transaction Envelope object.
  • Call Frame: The context and state for a specific function call during contract execution, including input parameters, local variables, and the execution environment.
  • Top-Level Call Frame: The initial execution context of a transaction accessing the contract, the “entry point” to the EVM code.
  • EIP-7701 Call Frame: A single atomic element of EVM code execution, represented by a single top-level call to a specific address with a given data. An EIP-7701 call frame may contain inner call frames as well, but they are not referred to as “EIP-7701 call frames”. An EIP-7701 call frame may either succeed or revert.
  • Frame’s Role: An identifier of an action that the invoked contract is asked to make during the current call frame. An Entity may have one or more roles as part of the EIP-7701 Transaction flow.
  • EIP-7701 Transaction Phase: A set of EIP-7701 Call Frames that form a single step in an EIP-7701 Transaction flow. There are two phases in an EIP-7701 Transaction: validation and execution
  • Validation phase: A set of EIP-7701 Call Frames that define the current EIP-7701 Transaction’s Validity by executing the validation EVM code.
  • Execution phase: A set of EIP-7701 Call Frames that perform the actions according to the Sender and the Paymaster contracts’ interpretation of the user input. These frames do not define the Validity of the transaction.

The base gas cost of this transaction is set to AA_BASE_GAS_COST instead of 21000 to reflect the lack of “intrinsic” ECDSA signature verification.

Validation and PostOp call frame roles

We define the following values as valid ones for the current_frame_role variable:

role_sender_deployment = 0xA0
role_sender_validation = 0xA1
role_paymaster_validation = 0xA2
role_sender_execution = 0xA3
role_paymaster_post_op = 0xA4

New ACCEPTROLE opcode

This opcode allows the invoked contract to explicitly accept executing a certain role as an entity in the EIP-7701 transaction. This opcode is treated same as the INVALID (0xFE) opcode if executed in a transaction of a different type.

It takes 3 stack argument:

  • role: the exact role this entity agrees to perform.
  • offset: byte offset in the memory in bytes, to copy what will be the return data of this context.
  • size: byte size of the return data to copy.

A valid call to the ACCEPTROLE opcode is an equivalent of the RETURN (0xF3) opcode and exits the current context successfully.

Global current_frame_role variable

During the execution of the Sender, Paymaster or a Deployer code as defined by the AA_TX_TYPE transaction, the global current_frame_role variable is set to the corresponding role. The current_frame_role remains set through an uninterrupted chain of DELEGATECALL calls.

By default, the value for current_frame_role is not set. Call frames initiated with any opcodes other than DELEGATECALL run without a role.

If by the end of the execution of the Sender, Paymaster or a Deployer code current_frame_role is not explicitly accepted by using the ACCEPTROLE opcode, the EIP-7701 Call Frame reverts.

An EIP-7701 transaction is valid if and only if the following conditions are met for each of role_sender_deployment, role_sender_validation, role_paymaster_validation:

  • The top-level call frame did not revert.
  • ACCEPTROLE was called exactly once and with the correct role input parameter equal to current_frame_role, either in the top level call frame, or in an uninterrupted chain of DELEGATECALL calls.

The role_sender_execution and role_paymaster_post_op EIP-7701 Call Frame is reverted if a corresponding ACCEPTROLE opcode was not executed, executed for a wrong role, or executed more than once.

New TXPARAMLOAD, TXPARAMSIZE, and TXPARAMCOPY opcodes

Accessing transaction details within call frames is performed using the new TXPARAM* opcode family. The instructions accept the parameter identifier value that we call txparam_id.

The TXPARAMDLOAD, TXPARAMSIZE, TXPARAMCOPY follow the pattern of CALLDATA* / RETURNDATA* opcode families.

  1. TXPARAMSIZE opcode puts the byte size of the transaction parameter value defined by the input txparam_id. It takes 1 stack argument:
    • txparam_id: identifier of the queried transaction parameter.
  2. TXPARAMLOAD opcode takes the byte offset in the specified transaction parameter from stack and puts the 32-byte value starting from the given offset of the specified transaction parameter to stack.
    It takes 2 stack argument:
    • txparam_id: identifier of the queried transaction parameter.
    • offset: byte offset in the transaction parameter to copy.
  3. TXPARAMCOPY opcode copies data from the appropriate dynamically sized transaction parameter to memory.
    It takes 4 stack arguments:
    • txparam_id: identifier of the queried transaction parameter.
    • dest_offset: byte offset in the memory where the result will be copied.
    • offset: byte offset in the transaction parameter to copy.
    • size: byte size to copy.

The valid values for txparam_id are described in the table below. Note that some parameters are “optional” for AA transactions, and these parameters have default values as stated below.

txparam_id Return value Data size Default Comment
0x00 current transaction type 32    
0x01 nonce 32    
0x02 sender 32    
0x03 sender_validation_data dynamic    
0x04 deployer 0 or 32 address(0)  
0x05 deployer_data dynamic empty array  
0x06 paymaster 0 or 32 address(0)  
0x07 paymaster_data dynamic empty array  
0x08 sender_execution_data dynamic    
0x09 valid_after 32 0  
0x0A valid_until 32 2^256-1  
0x0B max_priority_fee_per_gas 32    
0x0C max_fee_per_gas 32    
0x0D sender_validation_gas_limit 32    
0x0E paymaster_validation_gas_limit 32 0  
0x0F sender_execution_gas_limit 32    
0x10 paymaster_post_op_gas_limit 32 0  
0x11 access_list hash 32    
0x12 authorization_list hash 32    
0xF0 current_frame_role 32 0  
0xF1 tx_hash_for_signature 32   does not include sender_validation_data
0xF2 execution_status 32   only role_paymaster_post_op
0xF3 execution_gas_cost 32   only role_paymaster_post_op

Limitations on TXPARAM* opcodes

The TXPARAM* opcodes are only enabled in frames with a correct current_frame_role. Calling these opcodes in another context, or with invalid txparam_id, returns zero values and zero lengths.

current_frame_role allowed txparam_id
0 (none) None, TXPARAM* opcodes always return zero value and zero length
role_sender_deployment, role_sender_validation, role_paymaster_validation All tx parameters, current_frame_role, tx_hash_for_signature
role_paymaster_post_op All tx parameters, current_frame_role, tx_hash_for_signature, execution_status, execution_gas_cost
role_sender_execution Only current_frame_role

Contact should first call TXPARAMLOAD 0xF0 (current_frame_role) to determine the current frame role.

In case current_frame_role is not set for the current frame it has a default value of 0.

TXPARAM* opcodes gas prices

The new opcodes have gas prices equivalent to corresponding CALLDATA* opcodes:

  1. TXPARAMLOAD - static value of 3 gas
  2. TXPARAMSIZE- static value of 2 gas
  3. TXPARAMCOPY - dynamic formula

     minimum_word_size = (size + 31) / 32
     static_gas = 3
     dynamic_gas = 3 * minimum_word_size + memory_expansion_cost
    

Time range validity (optional)

If the valid_until field is non-zero, the transaction is only valid for inclusion in a block with a timestamp at most valid_until value. Similarly, the transaction is only valid for inclusion in blocks with a timestamp at most the valid_after value.

Sender Deployment (optional)

Inputs to the deployer contract are not defined by the protocol and are controlled by the deployer_data parameter.

The sender deployment frame MUST result in the sender address becoming initialized with contract code.

This step is performed with the role_sender_deployment role.

Sender Validation

This step is performed with the role_sender_validation role.

In order for the transaction to be considered valid, the sender validation frame MUST return without reverting.

Paymaster Validation (optional)

This step is performed with the role_paymaster_validation role.

In order for the transaction to be considered valid, the paymaster validation frame MUST return without reverting. If it does, the Paymaster contract is charged for the transaction gas costs instead of the Sender.

Sender Execution

This step is performed with the role_sender_execution role.

Inputs to the Sender contract are not defined by the protocol and are controlled by the sender_execution_data parameter.

Paymaster post-operation frame (optional)

This step is performed with the role_paymaster_post_op role.

It is intended to provide the Paymaster contract with an opportunity to finalize any calculations after the results of the Sender Execution are known.

The execution_status and execution_gas_cost values are accessible via the TXPARAMLOAD opcode.

The post-operation frame is considered an integral part of the transaction execution phase. It means that if the post-operation frame reverts its execution, the Sender Execution state changes are also reverted.

Transaction Execution Flow

All legacy transaction types only have an implicit validation phase where balance, nonce, and signature are checked, and an implicit execution phase with a single top-level execution frame.

For all legacy transaction types, during the single top-level execution frame, the ORIGIN (0x32, tx.origin) and CALLER (0x33, msg.sender) are both equal to the address that is determined by the transaction’s ECDSA signature (yParity, r, s).

When processing an EIP-7701 transaction, however, multiple execution frames will be created. The full list of possible frames and their corresponding role definitions is as follows:

  1. Validation Phase
    • sender deployment frame (once per account) - role_sender_deployment
    • sender validation frame (required) - role_sender_validation
    • paymaster validation frame (optional) - role_paymaster_validation
  2. Execution Phase
    • sender execution frame (required) - role_sender_execution
    • paymaster post-operation frame (optional) - role_paymaster_post_op

All execution frames in the Validation Phase must be completed successfully without reverting in order for the transaction to be considered valid for a given position in a block.

In all top-level frames, the global variables have the following meaning:

Opcode Name Solidity Equivalent Value
CALLER msg.sender The AA_ENTRY_POINT address
ORIGIN tx.origin The transaction sender address
CALLDATA* msg.data Empty for all call frames except for the sender execution frame, for which it is set to sender_execution_data

Transaction execution context

Note that some behaviours in the EVM depend on the transaction context. These behaviours include:

  1. Costs of the SSTORE (0x55) opcode per EIP-2200
  2. Costs of accessing cold addresses and slots per EIP-2929
  3. Values available within the transient storage per EIP-1153
  4. Maximum amount of gas refund assigned after the execution per EIP-3529

These features are not affected by the separation of the transaction into multiple frames. Meaning, for example, that a value set with TSTORE (0x5D) in one frame will remain available in the next one.

Costs of accessing cold addresses for Sender, Paymaster, and Deployer

The Sender address is pre-warmed as part of the AA_BASE_GAS_COST.

When non-zero address, that is not equal to the Sender address, is provided for a Paymaster or a Deployer contract, an additional EIP-2929 COLD_ACCOUNT_READ_COST cost of 2600 gas is charged and the address is added to accessed_addresses.

Flow diagrams

Simple AA Transaction flow

Simple AA transaction flow diagram

Complete AA transaction flow

Simple AA transaction flow diagram

Pseudocode

AA transaction state transition function


def state_transition_function(tx, block, state):
    max_gas = sum(tx.params[role].gas_limit for role in ROLES)
    gas_price = min(tx.max_fee_per_gas, block.base_fee_per_gas + tx.max_priority_fee_per_gas)
    total_max_cost = max_gas * gas_price
    if tx.paymaster is None:
        balances[tx.sender] -= total_max_cost
    else:
        balances[tx.paymaster] -= total_max_cost

    if get_code(tx.sender) is None:
        deployer_result = call_with_params(tx, role_sender_deployment)
        assert deployer_result.success

    checkpoint = state.take_snapshot()
    sender_result = call_with_params(tx, role_sender_validation)
    assert sender_result.success

    if tx.paymaster:
        paymaster_result = call_with_params(tx, role_paymaster_validation)
        assert paymaster_result.success

    call_with_params(tx, role_sender_execution)
    if tx.paymaster:
        postop_result = call_with_params(tx, role_paymaster_post_op)
        if postop_result.success is not True:
            state.revert_snapshot(checkpoint)
        balances[tx.paymaster] += gas_refund
    else:
        balances[tx.sender] += gas_refund


def call_with_params(tx, role):
    return call(
        AA_ENTRY_POINT, role, tx.params[role].address,
        tx.params[role].data, tx.params[role].gaslimit
    )


def call(from_address, target_role, target_address, call_data, gas_limit):
    result = execute_code_with_role(from_address, target_role, target_address, call_data, gas_limit)
    success = result.success and result.role_performed is True
    return {"success": success}


# This function is almost identical to the original EVM call frame transition function.
# In addition, it returns 'role_performed' value:
# True if the 'target_role' was accepted using the `ACCEPTROLE` opcode executed either in the top-level call,
# or for an inner call made with an uninterrupted 'DELEGATECALL' opcodes chain
# False otherwise
def execute_code_with_role(args):
    pass


# get the current contract code at the given address
def get_code(target_address):
    pass


# pre-charge the entire maximum gas cost of the transaction
def buy_gas(tx, block):
    max_gas = tx.sender_validation_gas_limit + tx.paymaster_validation_gas_limit + tx.sender_execution_gas_limit + tx.paymaster_post_op_gas_limit
    gas_price = min(tx.max_fee_per_gas, block.base_fee_per_gas + tx.max_priority_fee_per_gas)
    total_max_cost = max_gas * gas_price
    if tx.paymaster is None:
        balances[tx.paymaster] -= total_max_cost
    else:
        balances[tx.sender] -= total_max_cost


Block transition function

def block_transition_function(block):
    for index, tx in enumerate(block.txs):
        if is_type_aa(tx):
            try:
                if (tx.valid_until != 0 and tx.valid_until <= block.timestamp) or tx.valid_after >= block.timestamp:
                    raise Exception("time range violation")
                state_transition_function(tx, block)
            except Exception:
                # validation failed and this transaction could not have been included in the current block
                raise Exception("invalid AA Transaction in block")
        else:
            legacy_state_transition_function(tx)

Rationale

Introduction of the TXPARAM* opcode family

The validation calls of a Smart Contract Account code need to have full access to the majority of transaction details in order to be able to make an informed decision about either accepting or rejecting the transaction.

A small subset of this data is available with the existing opcodes, like CALLER (0x33) or GASPRICE (0x3A). However, creating an opcode for every transaction parameter is not feasible or desirable.

The TXPARAM* opcode family provides the Account Abstraction contracts with access to this data.

These values are not made accessible to the transactions’ execution or to legacy transaction types. This limitation prevents the TXPARAM* opcode family from becoming a new source of a globally observable state, which could create backwards compatibility issues in the future.

Backwards Compatibility

Security Considerations

As the ACCEPTROLE opcode represent a generic way to authorize any action on behalf of the contract, correct and secure implementation of this code is critical. We expect that compilers targeting EVM will play a major role in enabling and ensuring Smart Contract Accounts’ security.

For smart contract security auditors and security-oriented developer tools it is crucial to ensure that contracts not meant to have roles in AA transactions do not have unexpected ACCEPTROLE opcode. Otherwise, these contracts may present an immediate security threat.

As an example, block explorers should tag contracts as “user accounts” or “paymasters” if they have the ACCEPTROLE opcode used in their source code.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), "EIP-7701: Native Account Abstraction [DRAFT]," Ethereum Improvement Proposals, no. 7701, May 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7701.