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”.
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:
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.
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.
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.
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
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:
Validation Phase
sender deployment frame (once per account) - role_sender_deployment
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:
Costs of accessing cold addresses and slots per EIP-2929
Values available within the transient storage per EIP-1153
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
Complete AA transaction flow
Pseudocode
AA transaction state transition function
defstate_transition_function(tx,block,state):max_gas=sum(tx.params[role].gas_limitforroleinROLES)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_priceiftx.paymasterisNone:balances[tx.sender]-=total_max_costelse:balances[tx.paymaster]-=total_max_costifget_code(tx.sender)isNone:deployer_result=call_with_params(tx,role_sender_deployment)assertdeployer_result.successcheckpoint=state.take_snapshot()sender_result=call_with_params(tx,role_sender_validation)assertsender_result.successiftx.paymaster:paymaster_result=call_with_params(tx,role_paymaster_validation)assertpaymaster_result.successcall_with_params(tx,role_sender_execution)iftx.paymaster:postop_result=call_with_params(tx,role_paymaster_post_op)ifpostop_result.successisnotTrue:state.revert_snapshot(checkpoint)balances[tx.paymaster]+=gas_refundelse:balances[tx.sender]+=gas_refunddefcall_with_params(tx,role):returncall(AA_ENTRY_POINT,role,tx.params[role].address,tx.params[role].data,tx.params[role].gaslimit)defcall(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.successandresult.role_performedisTruereturn{"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
defexecute_code_with_role(args):pass# get the current contract code at the given address
defget_code(target_address):pass# pre-charge the entire maximum gas cost of the transaction
defbuy_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_limitgas_price=min(tx.max_fee_per_gas,block.base_fee_per_gas+tx.max_priority_fee_per_gas)total_max_cost=max_gas*gas_priceiftx.paymasterisNone:balances[tx.paymaster]-=total_max_costelse:balances[tx.sender]-=total_max_cost
Block transition function
defblock_transition_function(block):forindex,txinenumerate(block.txs):ifis_type_aa(tx):try:if(tx.valid_until!=0andtx.valid_until<=block.timestamp)ortx.valid_after>=block.timestamp:raiseException("time range violation")state_transition_function(tx,block)exceptException:# validation failed and this transaction could not have been included in the current block
raiseException("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.