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.
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.
Name | Value |
---|---|
AA_TX_TYPE | TBD |
AA_ENTRY_POINT | address(0x7701) |
AA_BASE_GAS_COST | 15000 |
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
])
Sender
contract.Sender
contract if necessary in the context of
the current AA transaction.Sender
Smart Contract Account and represented with
an EIP-2718 compatible Transaction Envelope object.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.
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
ACCEPTROLE
opcodeThis 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.
current_frame_role
variableDuring 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
:
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.
TXPARAMLOAD
, TXPARAMSIZE
, and TXPARAMCOPY
opcodesAccessing 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 |
TXPARAM*
opcodesThe 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 pricesThe new opcodes have gas prices equivalent to corresponding CALLDATA*
opcodes:
TXPARAMLOAD
- static value of 3
gasTXPARAMSIZE
- static value of 2
gasTXPARAMCOPY
- dynamic formula
minimum_word_size = (size + 31) / 32
static_gas = 3
dynamic_gas = 3 * minimum_word_size + memory_expansion_cost
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.
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.
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.
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
.
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.
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.
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:
sender
deployment frame (once per account) - role_sender_deployment
sender
validation frame (required) - role_sender_validation
paymaster
validation frame (optional) - role_paymaster_validation
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 |
Note that some behaviours in the EVM depend on the transaction context. These behaviours include:
SSTORE (0x55)
opcode per EIP-2200These 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.
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
.
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
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
checkpoint = state.take_snapshot()
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
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)
TXPARAM*
opcode familyThe 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.
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.