EIP2938 - Account Abstraction
# Simple Summary
Account abstraction (AA) allows a contract to be the top-level account that pays fees and starts transaction execution.
# Abstract
See also: https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020 (opens new window) and the links therein for historical work and motivation.
Transaction validity, as of Muir Glacier, is defined rigidly by the protocol: ECDSA signature, a simple nonce, and account balance. Account abstraction extends the validity conditions of transactions with the execution of arbitrary EVM bytecode (with some limits on what state may be accessed.) To signal validity, we propose a new EVM opcode PAYGAS
, which also sets the gas price and gas limit the contract is willing to pay.
We split account abstraction into two tiers: single-tenant AA, which is intended to support wallets or other use cases with few participants, and multi-tenant AA, which is intended to support applications with many participants (eg. tornado.cash, Uniswap).
# Motivation
The existing limitations preclude innovation in a number of important areas, particularly:
- Smart contract wallets that use signature verification other than ECDSA (eg. Schnorr, BLS, post-quantum...)
- Smart contract wallets that include features such as multisig verification or social recovery, reducing the highly prevalent risk of funds being lost or stolen
- Privacy-preserving systems like tornado.cash (opens new window)
- Attempts to improve gas efficiency of DeFi protocols by preventing transactions that don't satisfy high-level conditions (eg. existence of a matching order) from being included on chain
- Users being able to pay for transaction fees in a token other than ETH (eg. by converting that token into the ETH needed for fees inside the transaction in real-time)
Most of the above use cases are currently possible using intermediaries, most notably the Gas Station Network (opens new window) and application-specific alternatives. These implementations are (i) technically inefficient, due to the extra 21000 gas to pay for the relayer, (ii) economically inefficient, as relayers need to make a profit on top of the gas fees that they pay. Additionally, use of intermediary protocols means that these applications cannot simply rely on base Ethereum infrastructure and need to rely on extra protocols that have smaller userbases and higher risk of no longer being available at some future date.
Out of the five use cases above, single-tenant AA approximately supports (1) and (2), and multi-tenant AA approximately supports (3) and (4). We discuss the differences between the two tiers in the specification and rationale sections below.
# Specification
# Single Tenant
After FORK_BLOCK
, the following changes will be recognized by the protocol.
# Constants
Constant | Value |
---|---|
AA_ENTRY_POINT | 0xffffffffffffffffffffffffffffffffffffffff |
AA_TX_TYPE | 2 |
FORK_BLOCK | TBD |
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 rlp([nonce, target, data])
.
The base gas cost of this transaction is set to AA_BASE_GAS_COST
instead of 21000 to reflect the lack of "intrinsic" ECDSA and signature.
Nonces are processed analogously to existing transactions (check tx.nonce == tx.target.nonce
, transaction is invalid if this fails, otherwise proceed and immediately set tx.nonce += 1
).
Note that this transaction type has no intrinsic gas limit; when beginning execution, the gas limit is simply set to the remaining gas in the block (ie. block.gas_limit
minus gas spent on previous transactions), and the PAYGAS
opcode (see below) can adjust the gas limit downwards.
# Transaction-wide global variables
Introduce some new transaction-wide global variables. These variables work similarly (in particular, have similar reversion logic) to the SSTORE refunds counter.
Variable | Type | Initial value |
---|---|---|
globals.transaction_fee_paid | bool | False if type(tx) == AA_TX_TYPE else True |
globals.gas_price | int | 0 if type(tx) == AA_TX_TYPE else tx.gas_price |
globals.gas_limit | int | 0 if type(tx) == AA_TX_TYPE else tx.gas_limit |
# NONCE (0x48)
Opcode
A new opcode NONCE (0x48)
is introduced, with gas cost G_base
, which pushes the nonce
of the callee onto the stack.
# PAYGAS (0x49)
Opcode
A new opcode PAYGAS (0x49)
is introduced, with gas cost G_base
. It takes two arguments off the stack: (top) version_number
, (second from top) memory_start
. In the initial implementation, it will assert version_number == 0
and read:
gas_price = bytes_to_int(vm.memory[memory_start: memory_start + 32])
gas_limit = bytes_to_int(vm.memory[memory_start + 32: memory_start + 64])
Both reads use similar mechanics to MLOAD and CALL, so memory expands if needed.
Future hard forks may add support for different version numbers, in which case the opcode may take different-sized memory slices and interpret them differently. Two particular potential use cases are EIP 1559 (opens new window) and the escalator mechanism (opens new window).
The opcode works as follows. If all three of the following conditions (in addition to the version number check above) are satisfied:
- The account's balance is
>= gas_price * gas_limit
globals.transaction_fee_paid == False
- We are in a top-level AA execution frame (ie. if the currently running EVM execution exits or reverts, the EVM execution of the entire transaction is finished)
Then do the following:
- Subtract
gas_price * gas_limit
from the contract's balance - Set
globals.transaction_fee_paid
toTrue
- Set
globals.gas_price
togas_price
, andglobals.gas_limit
togas_limit
- Set the remaining gas in the current execution context to equal
gas_limit
minus the gas that was already consumed
If any of the above three conditions are not satisfied, throw an exception.
At the end of execution of an AA transaction, it is mandatory that globals.transaction_fee_paid == True
; if it is not, then the transaction is invalid. At the end of execution, the contract is refunded globals.gas_price * remaining_gas
for any remaining gas, and (globals.gas_limit - remaining_gas) * globals.gas_price
is transferred to the miner.
PAYGAS
also serves as an EVM execution checkpoint: if the top-level execution frame reverts after PAYGAS
has been called, then the execution only reverts up to the point right after PAYGAS
was called, and exits there. In that case, the contract receives no refund, and globals.gas_limit * globals.gas_price
is transferred to the miner.
# Replay Protection
One of the two following approaches must be implemented to safeguard against replay attacks.
# Require SET_INDESTRUCTIBLE
Require that contracts targeted by AA transactions begin with EIP-2937's SET_INDESTRUCTIBLE
opcode. AA transactions targeting contracts that do not begin with SET_INDESTRUCTIBLE
are invalid, and cannot be included in blocks.
AA_PREFIX
would need to be modified to include this opcode.
# Preserve Nonce on SELFDESTRUCT
The other option is to preserve contract nonces across SELFDESTRUCT
invocations, instead of setting the nonce to zero.
# Miscellaneous
- If
CALLER (0x33)
is invoked in the first frame of execution of a call initiated by an AA transaction, then it must returnAA_ENTRY_POINT
. - If
ORIGIN (0x32)
is invoked in any frame of execution of an AA transaction it must returnAA_ENTRY_POINT
. - The
GASPRICE (0x3A)
opcode now pushes the valueglobals.gas_price
Note that the new definition of GASPRICE
does not lead to any changes in behavior in non-AA transactions, because globals.gas_price
is initialized to tx.gas_price
and cannot be changed as PAYGAS
cannot be called.
# Mining and Rebroadcasting Strategies
Much of the complexity in account abstraction originates from the strategies used by miners and validating nodes to determine whether or not to accept and rebroadcast transactions. Miners need to determine if a transaction will actually pay the fee if they include it after only a small amount of processing to avoid DoS attacks (opens new window). Validating nodes need to perform an essentially identical verification to determine whether or not to rebroadcast the transaction.
By keeping the consensus changes minimal, this EIP allows for gradual introduction of AA mempool support by miners and validating nodes. Initial support would be focused on enabling simple, single-tenant use cases, while later steps would additionally allow for more complex, multi-tenant use cases. Earlier stages are deliberately more fully fleshed-out than later stages, as there is still more time before later stages need to be implemented.
# Transactions with Fixed Nonces
Constant | Value |
---|---|
VERIFICATION_GAS_MULTIPLIER | 6 |
VERIFICATION_GAS_CAP | = VERIFICATION_GAS_MULTIPLIER * AA_BASE_GAS_COST = 90000 |
AA_PREFIX | if(msg.sender != shr(-1, 12)) { LOG1(msg.sender, msg.value); return } ; compilation to EVM TBD |
When a node receives an AA transaction, they process it (i.e. attempt to execute it against the current chain head's post-state) to determine its validity, continuing to execute until one of several events happens:
- If the code of the
target
is NOT prefixed withAA_PREFIX
, exit with failure - If the execution hits any of the following, exit with failure:
- An environment opcode (
BLOCKHASH
,COINBASE
,TIMESTAMP
,NUMBER
,DIFFICULTY
,GASLIMIT
) BALANCE
(of any account, including thetarget
itself)- An external call/create that changes the
callee
to anything but thetarget
or a precompile (CALL
,CALLCODE
,STATICCALL
,CREATE
,CREATE2
). - An external state access that reads code (
EXTCODESIZE
,EXTCODEHASH
,EXTCODECOPY
, but alsoCALLCODE
andDELEGATECALL
), unless the address of the code that is read is thetarget
.
- An environment opcode (
- If the execution consumes more gas than
VERIFICATION_GAS_CAP
(specified above), or more gas than is available in the block, exit with failure - If the execution reaches
PAYGAS
, then exit with success or failure depending on whether or not the balance is sufficient (e.g.balance >= gas_price * gas_limit
).
Nodes do not keep transactions with nonces higher than the current valid nonce in the mempool. If the mempool already contains a transaction with a currently valid nonce, another incoming transaction to the same contract and with the same nonce either replaces the existing one (if its gas price is sufficiently higher) or is dropped. Thus, the mempool keeps only at most one pending transaction per account.
While processing a new block, take note of which accounts were the target
of an AA transaction (each block currently has 12500000
gas and an AA transaction costs >= 15000
so there would be at most 12500000 // 15000 = 833
targeted accounts). Drop all pending transactions targeting those accounts. All other transactions remain in the mempool.
# Single Tenant+
If the indestructible contracts EIP (opens new window) is added, Single Tenant AA can be adapted to allow for DELEGATECALL
during transaction verification: during execution of a new AA transaction, external state access that reads code (EXTCODESIZE
, EXTCODEHASH
, EXTCODECOPY
, CALLCODE
, DELEGATECALL
) of any contract whose first byte is the SET_INDESTRUCTIBLE
opcode is no longer banned. However, calls to anything but the target
or a precompile that change the callee
(i.e., calls other than CALLCODE
and DELEGATECALL
) are still not permitted.
If the IS_STATIC EIP (opens new window) is added, the list of allowed prefixes can be extended to allow a prefix that enables incoming static calls but not state-changing calls.
The list of allowed prefixes can also be extended to enable other benign use cases (eg. logging incoming payments).
External calls into AA accounts can be allowed as follows. We can add an opcode RESERVE_GAS
, which takes as argument a value N
and has simple behavior: immediately burn N
gas and add N
gas to the refund. We then add an allowed AA_PREFIX
that reserves >= AA_BASE_GAS_COST * 2
gas. This ensures that at least AA_BASE_GAS_COST
gas must be spent (as refunds can refund max 50% of total consumption) in order to call into an account and invalidate transactions targeting that account in the mempool, preserving that invariant.
Note that accounts may also opt to set a higher RESERVE_GAS
value in order to safely have a higher VERIFICATION_GAS_CAP
; the goal would be to preserve a VERIFICATION_GAS_MULTIPLIER
-to-1 ratio between the minimum gas cost to edit an account (ie. half its RESERVE_GAS
) and the VERIFICATION_GAS_CAP
that is permitted that account. This would also preserve invariants around maximum reverification gas consumption that are implied by the previous section.
# Multi-Tenant & Beyond
In a later stage, we can add support for multiple pending transactions per account in the mempool. The main challenge here is that a single transaction can potentially cause state changes that invalidate all other transactions to that same account. Additionally, if we naively prioritize transactions by gasprice, there is an attack vector where the user willing to pay the highest gasprice publishes many (mutually exclusive) versions of their transaction with small alterations, thereby pushing everyone else's transactions out of the mempool.
Here is a sketch of a strategy for mitigating this problem. We would require incoming transactions to contain an EIP-2930 (opens new window)-style access list detailing the storage slots that the transaction reads or modifies, and make it binding; that is, accesses outside the access list would be invalid. A transaction would only be included in the mempool if its access list is disjoint from the access lists of other transactions in the mempool (or if its gasprice is higher). An alternative way to think about this is to have per-storage-slot mempools instead of just per-account mempools, except a transaction could be part of multiple per-storage-slot mempools (if desired it could be capped to eg. 5 storage slots).
Note also that multi-tenant AA will almost certainly require allowing miners to edit the nonces of incoming transactions to put them into sequence, with the result that the final hash of a transaction is unpredictable at publication time. Clients will need to explicitly work around this.
More research is required to refine these ideas, and this is left for later work.
# Rationale
The core problem in an account abstraction setup is always that miners and network nodes need to be able to verify that a transaction that they attempt to include, or rebroadcast, will actually pay a fee. Currently, this is fairly simple, because a transaction is guaranteed to be includable and pay a fee as long as the signature and nonce are valid and the balance and gasprice are sufficient. These checks can be done quickly.
In an account abstraction setup, the goal is to allow accounts to specify EVM code that can establish more flexible conditions for a transaction's validity, but with the requirement that this EVM code can be quickly verified, with the same safety properties as the existing setup.
In a normal transaction, the top-level call goes from the tx.sender
to tx.to
and carries with it tx.value
. In an AA transaction, the top-level call goes from the entry point address (0xFFFF...FF
) to the tx.target
.
The top-level code execution is expected to be split into two phases: the shorter verification phase (before PAYGAS
) and the longer execution phase (after PAYGAS
). If execution throws an exception during the verification phase, the transaction is invalid, much like a transaction with an invalid signature in the current system. If execution throws an exception after the verification phase, the transaction pays fees, and so the miner can still include it.
The transition between different stages of AA is entirely done through changes in miner strategy. The first stage supports single-tenant AA, where the only use cases that can be easily implemented are where the tx.target
is a contract representing a user account (that is, a smart contract wallet, eg. multisig). Later stages improve support for eg. logs and libraries, and also move toward supporting multi-tenant AA, where the goal is to try to support cases where the tx.target
represents an application that processes incoming activity from multiple users.
# Nonces still enshrined in single-tenant AA
Nonces are still enforced in single-tenant AA to ensure that single-target AA does not break the invariant that each transaction (and hence each transaction hash) can only be included in the chain once. While there is some limited value in allowing arbitrary-order transaction inclusion in single-tenant AA, there is not enough value to justify breaking that invariant.
Note that nonces in AA accounts do end up having a dual-purpose: they are both there for replay protection and for contract address generation when using the CREATE
opcode. This does mean that a single transaction could increment the nonce by more than 1. This is deemed acceptable, as the other mechanics introduced by AA already break the ability to easily verify that a chain longer than one transaction can be processed. However, we strongly recommend that AA contracts use CREATE2
instead of CREATE
.
In multi-tenant AA, as mentioned above, nonces are expected to become malleable and applications that use multi-tenant AA systems would need to manage this.
# Nonces are exposed to the EVM
This is done to allow signature checking done in validation code to validate the nonce.
# Replay Protection
One of the above two approaches (requiring SET_INDESTRUCTIBLE
or modifying SELFDESTRUCT
behavior) must be implemented so that nonces cannot be reused. It must be a consensus change, and not simply part of AA_PREFIX
, so that transaction hash uniqueness is maintained.
# Miners refuse transactions that access external data or the target's own balance, before PAYGAS
An important property of traditional transactions is that activity happening as part of transactions that originate outside of some given account X cannot make transactions whose sender is X invalid. The only state change that an outside transaction can impose on X is increasing its balance, which cannot invalidate a transaction.
Allowing AA contracts to access external data (both other accounts and environment variables such as GASPRICE, DIFFICULTY, etc.) before they call PAYGAS
(ie. during the verification phase) breaks this invariant. For example, imagine someone sends many thousands of AA transactions that perform an external call if FOO.get_number() != 5: throw()
. FOO.number
might be set to 5
when those transactions are all sent, but a single transaction to FOO
could set the number
to something else, invalidating all of the thousands of AA transactions that depend on it. This would be a serious DoS vector.
The one allowed exception is contracts that are indestructible (that is, whose first byte is the SET_INDESTRUCTIBLE
opcode defined in this EIP (opens new window)). This is a safe exception, because the data that is being read cannot be changed.
Disallowing reading BALANCE
blocks a milder attack vector: an attacker could force a transaction to be reprocessed at a mere cost of 6700 gas (not 15000 or 21000), in the worst case more than doubling the number of transactions that would need to be reprocessed.
In the long term, AA could be expanded to allow reading external data, though protections such as mandatory access lists would be required.
# AA transactions must call contracts with prefix
The prelude is used to ensure that only AA transactions can call the contract. This is another measure taken to ensure the invariant described above. If this check did not occur, it would be possible for a transaction originating outside some AA account X to call into X and make a storage change, forcing transactions targeting that account to be reprocessed at the cost of a mere 5000 gas.
# Multi-tenant AA
Multi-tenant AA extends single-tenant AA by better handling cases where distinct and uncoordinated users attempt to send transactions for/to the same account and those transactions may interfere with each other.
We can understand the value of multi-tenant AA by examining two example use cases: (i) tornado.cash (opens new window) and (ii) Uniswap (opens new window). In both of these cases, there is a single central contract that represents the application, and not any specific user. Nevertheless, there is important value in using abstraction to do application-specific validation of transactions.
# Tornado Cash
The tornado.cash workflow is as follows:
- A user sends a transaction to the TC contract, depositing some standard quantity of coins (eg. 1 ETH). A record of their deposit, containing the hash of a secret known by the user, is added to a Merkle tree whose root is stored in the TC contract.
- When that user later wants to withdraw, they generate and send a ZK-SNARK proving that they know a secret whose hash is in a leaf somewhere in the deposit tree (without revealing where). The TC contract verifies the ZK-SNARK, and also verifies that a nullifier value (also derivable from the secret) has not yet been spent. The contract sends 1 ETH to the user's desired address, and saves a record that the user's nullifier has been spent.
The privacy provided by TC arises because when a user makes a withdrawal, they can prove that it came from some unique deposit, but no one other than the user knows which deposit it came from. However, implementing TC naively has a fatal flaw: the user usually does not yet have ETH in their withdrawal address, and if the user uses their deposit address to pay for gas, that creates an on-chain link between their deposit address and their withdrawal address.
Currently, this is solved via relayers; a third-party relayer verifies the ZK-SNARK and unspent status of the nullifier, publishes the transaction using their own ETH to pay for gas, and collects the fee back from the user from the TC contract.
AA allows this without relayers: the user could simply send an AA transaction targeting the TC contract, the ZK-SNARK verification and the nullifier checking can be done in the verification step, and PAYGAS can be called directly after that. This allows the withdrawer to pay for gas directly out of the coins going to their withdrawal address, avoiding the need for relayers or for an on-chain link to their deposit address.
Note that fully implementing this functionality requires AA to be structured in a way that supports multiple users sending withdrawals at the same time (requiring nonces would make this difficult), and that allows a single account to support both AA transactions (the withdrawals) and externally-initiated calls (the deposits).
# Uniswap
A new version of Uniswap could be built that allows transactions to be sent that directly target the Uniswap contract. Users could deposit tokens into Uniswap ahead of time, and Uniswap would store their balances as well as a public key that transactions spending those balances could be verified against. An AA-initiated Uniswap trade would only be able to spend these internal balances.
This would be useless for normal traders, as normal traders have their coins outside the Uniswap contract, but it would be a powerful boon to arbitrageurs. Arbitrageurs would deposit their coins into Uniswap, and they would be able to send transactions that perform arbitrage every time external market conditions change, and logic such as price limits could be enforced during the verification step. Hence, transactions that do not get in (eg. because some other arbitrageur made the trade first) would not be included on-chain, allowing arbitrageurs to not pay gas, and reducing the number of "junk" transactions that get included on-chain. This could significantly increase both de-facto blockchain scalability as well as market efficiency, as arbitrageurs would be able to much more finely correct for cross-exchange discrepancies between prices.
Note that here also, Uniswap would need to support both AA transactions and externally-initiated calls.
# Backwards Compatibility
This AA implementation preserves the existing transaction type. The use of assert origin == caller
to verify that an account is an EOA remains sound, but is not extensible to AA accounts; AA transactions will always have origin == AA_ENTRY_POINT
.
Badly-designed single-tenant AA contracts will break the transaction non-malleability invariant. That is, it is possible to take an AA transaction in-flight, modify it, and have the modified version still be valid; AA account contracts can be designed in such a way as to make that not possible, but it is their responsibility. Multi-tenant AA will break the transaction non-malleability invariant much more thoroughly, making the transaction hash unpredictable even for legitimate applications that use the multi-tenant AA features (though the invariant will not further break for applications that existed before then).
AA contracts may not have replay protection unless they build it in explicitly; this can be done with the CHAINID (0x46)
opcode introduced in EIP 1344.
# Test Cases
See: https://github.com/quilt/tests/tree/account-abstraction (opens new window)
# Implementation
See: https://github.com/quilt/go-ethereum/tree/account-abstraction (opens new window)
# Security Considerations
See https://ethresear.ch/t/dos-vectors-in-account-abstraction-aa-or-validation-generalization-a-case-study-in-geth/7937 (opens new window) for an analysis of DoS issues.
# Re-validation
When a transaction enters the mempool, the client is able to quickly ascertain whether the transaction is valid. Once it determines this, it can be confident that the transaction will continue to be valid unless a transaction from the same account invalidates it.
There are, however, cases where an attacker can publish a transaction that invalidates existing transactions and requires the network to perform more recomputation than the computation in the transaction itself. The EIP maintains the invariant that recomputation is bounded to a theoretical maximum of six times the block gas limit in a single block; this is somewhat more expensive than before, but not that much more expensive.
# Peer denial-of-service
Denial-of-Service attacks are difficult to defend against, due to the difficulty in identifying sybils within a peer list. At any moment, one may decide (or be bribed) to initiate an attack. This is not a problem that Account Abstraction introduces. It can be accomplished against existing clients today by inundating a target with transactions whose signatures are invalid. However, due to the increased allotment of validation work allowed by AA, it's important to bound the amount of computation an adversary can force a client to expend with invalid transactions. For this reason, it's best for the miner to follow the recommended mining strategies.
# Copyright
Copyright and related rights waived via CC0 (opens new window).