EIP-2938: Account Abstraction Source

AuthorVitalik Buterin, Ansgar Dietrichs, Matt Garnett, Will Villanueva, Sam Wilson
Discussions-Tohttps://ethereum-magicians.org/t/eip-2938-account-abstraction/4630
StatusDraft
TypeStandards Track
CategoryCore
Created2020-09-04
Requires 2718

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 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:

  1. Smart contract wallets that use signature verification other than ECDSA (eg. Schnorr, BLS, post-quantum…)
  2. Smart contract wallets that include features such as multisig verification or social recovery, reducing the highly prevalent risk of funds being lost or stolen
  3. Privacy-preserving systems like tornado.cash
  4. 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
  5. 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 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) Opcodeissues

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 and the escalator mechanism.

The opcode works as follows. If all three of the following conditions (in addition to the version number check above) are satisfied:

  1. The account’s balance is >= gas_price * gas_limit
  2. globals.transaction_fee_paid == False
  3. 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 to True
  • Set globals.gas_price to gas_price, and globals.gas_limit to gas_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.

Miscellaneous

  • If CALLER (0x33) is invoked in the first frame of execution of a call initiated by an AA transaction, then it must return AA_ENTRY_POINT.
  • If ORIGIN (0x32) is invoked in any frame of execution of an AA transaction it must return AA_ENTRY_POINT.
  • The GASPRICE (0x3A) opcode now pushes the value globals.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. 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 with AA_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 the target itself)
    • An external call/create that changes the callee to anything but the target or a precompile (CALL, CALLCODE, STATICCALL, CREATE, CREATE2).
    • An external state access that reads code (EXTCODESIZE, EXTCODEHASH, EXTCODECOPY, but also CALLCODE and DELEGATECALL), unless the address of the code that is read is the target.
  • 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 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) to any contract whose first byte is the SET_INDESTRUCTIBLE opcode is no longer banned. However, calls other than CALLCODE and DELEGATECALL (ie. calls that change the callee) to anything but the target or a precompile are still not permitted.

If the IS_STATIC EIP 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-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 includeable 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.

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…) 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). 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 and (ii) Uniswap. 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:

  1. 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.
  2. 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 nulllifier, publish the transaction using their own ETH to pay for gas, and collects the fee back from the user from the TC contract.

AA allows us to do this without relayers: the user could simply send an AA transaction targeting the TC contract, and the ZK-SNARK verification and the nullifier checking can be done as 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).

All AA contracts may also potentially 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

Implementation

See: https://github.com/quilt/go-ethereum/tree/account-abstraction

Security Considerations

See https://ethresear.ch/t/dos-vectors-in-account-abstraction-aa-or-validation-generalization-a-case-study-in-geth/7937 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 increased allotment of validation work allowed to AA, it’s important 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 and related rights waived via CC0.

Citation

Please cite this document as:

Vitalik Buterin, Ansgar Dietrichs, Matt Garnett, Will Villanueva, Sam Wilson, "EIP-2938: Account Abstraction [DRAFT]," Ethereum Improvement Proposals, no. 2938, September 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2938.