Native Account Abstraction
相关视频
正文
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.
A more detailed motivation for this proposal can be found in the README document.
Specification
Constants
Name | Value |
---|---|
AA_TX_TYPE | TBD |
AA_ENTRY_POINT | address(0x7701) |
AA_BASE_GAS_COST | 15000 |
ROLE_SENDER_DEPLOYMENT | 0xA0 |
ROLE_SENDER_VALIDATION | 0xA1 |
ROLE_PAYMASTER_VALIDATION | 0xA2 |
ROLE_SENDER_EXECUTION | 0xA3 |
ROLE_PAYMASTER_POST_OP | 0xA4 |
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,
max_priority_fee_per_gas, max_fee_per_gas,
sender_validation_gas, paymaster_validation_gas,
sender_execution_gas, paymaster_post_op_gas,
access_list,
authorization_list
])
CURRENT_ROLE
and ACCEPT_ROLE
opcodes
current_context_role
is a context variable set by the AA transaction to the current role.
During AA transactions, it is set to the current role in the transaction's lifecycle for each top level call.
During non-AA transactions it is always set to ROLE_SENDER_EXECUTION
.
It remains unchanged on DELEGATECALL
but is reset to ROLE_SENDER_EXECUTION
on CALL
/ STATICCALL
/ CALLCODE
. This behavior resembles msg.sender
.
The CURRENT_ROLE
opcode returns the current_context_role
value.
The ACCEPT_ROLE
opcode is equivalent to RETURN
in the sense that it copies a memory slice, ends execution, and pastes the memory slice onto parent returndata, with a single modification:
- It accepts the
frame_role
as the additional input parameter, and reverts if it differs from thecurrent_context_role
For each role
in the transaction's lifecycle, a successful ACCEPT_ROLE
is expected with the frame_role == role
.
If any validation frame failed to perform an ACCEPT_ROLE
matching its role, the transaction fails the validity checks and cannot be included.
TXPARAM*
opcodes
The TXPARAMDLOAD
, TXPARAMSIZE
, TXPARAMCOPY
opcodes follow the pattern of CALLDATA*
/ RETURNDATA*
opcode families.
Each TXPARAM*
opcode takes an extra stack input value as a first input compared to its CALLDATA*
equivalent.
The values of this input are as follows:
n | 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 | ||
0x0B | max_priority_fee_per_gas | 32 | ||
0x0C | max_fee_per_gas | 32 | ||
0x0D | sender_validation_gas | 32 | ||
0x0E | paymaster_validation_gas | 32 | 0 | |
0x0F | sender_execution_gas | 32 | ||
0x10 | paymaster_post_op_gas | 32 | 0 | |
0x11 | access_list hash | 32 | ||
0x12 | authorization_list hash | 32 | ||
0xf1 | execution_status | 32 | See transaction scoped vars in processing flow | |
0xf2 | execution_gas_used | 32 | See transaction scoped vars in processing flow | |
0xff | tx_hash_for_signature | 32 | Hash of the transaction without the signature |
Affected opcodes
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 |
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 a non-zero address that is not equal to the Sender
address, is provided for a Paymaster
or a Deployer
contract,
an additional EIP-2930 ACCESS_LIST_ADDRESS_COST
cost of 2400 gas is charged and the address is added to accessed_addresses
.
AA transaction processing flow
We define processing flow for an AA transaction as follows:
def state_transition_function(tx, block, state):
# Empty refunds, warm list, execution status and gas used (new), etc
state.transaction_scoped_vars = {}
max_gas = tx.sender_validation_gas + tx.paymaster_validation_gas + tx.sender_execution_gas + tx.paymaster_post_op_gas
gas_price = min(tx.max_fee_per_gas, block.base_fee_per_gas + tx.max_priority_fee_per_gas)
payer = tx.sender if tx.paymaster is None else tx.paymaster
total_max_cost = max_gas * gas_price
balances[payer] -= total_max_cost
gas_used = 0
if get_code(tx.sender) is None:
deployer_result = call(tx.deployer, [], tx.sender_validation_gas_limit, ROLE_SENDER_DEPLOYMENT)
assert deployer_result.accepted_role == ROLE_SENDER_DEPLOYMENT
gas_used += deployer_result.gas_used
sender_result = call(tx.sender, [], tx.sender_validation_gas_limit - gas_used, ROLE_SENDER_VALIDATION)
assert sender_result.accepted_role == ROLE_SENDER_VALIDATION
gas_used += sender_result.gas_used
if tx.paymaster:
paymaster_result = call(tx.paymaster, [], tx.paymaster_validation_gas, ROLE_PAYMASTER_VALIDATION)
assert paymaster_result.accepted_role == ROLE_PAYMASTER_VALIDATION
gas_used += paymaster_result.gas_used
checkpoint = state.take_snapshot()
sender_execution_result = call(tx.sender, [], tx.sender_execution_gas, ROLE_SENDER_EXECUTION)
gas_used += sender_execution_result.gas_used
state.transaction_scoped_vars[execution_status] = sender_execution_result.output_code
state.transaction_scoped_vars[execution_gas_used] = gas_used
if tx.paymaster:
postop_result = call(tx.paymaster, [], tx.paymaster_post_op_gas, ROLE_PAYMASTER_POST_OP)
gas_used += postop_result.gas_used
if postop_result.accepted_role != ROLE_PAYMASTER_POST_OP:
state.revert_snapshot(checkpoint)
balances[payer] += gas_price * (max_gas - gas_used)
Rationale
A full list of rationales for the decisions made in this proposal can be found in the README document.
Backwards Compatibility
Security Considerations
As the ACCEPT_ROLE
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 ACCEPT_ROLE
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 ACCEPT_ROLE
opcode used in their source code.
Copyright
Copyright and related rights waived via CC0.