# Account Abstraction in zkSync 2.0 *A special thank you to the authors of EIP-2938 and EIP-4337, whose years of research has made this proposal possible.* ## Introduction ### What is Account Abstraction? EVM code is used to implement the logic of applications, but what if it could also be used to implement the verification logic (nonces, signatures…) of individual users’ wallets? For simplicity, Ethereum currently hardcodes the verification logic to a nonce and ECDSA signature, but this means transactions can only “start from” an externally owned account (EOA). Account abstraction (AA) allows a contract to be a top-level account that pays fees and starts transaction execution. ![](https://i.imgur.com/7Fry01r.png) ### Motivating Use Cases 1. Natural implementations of **smart contract wallets** (eg. social recovery, multisig). 2. Paying **transaction fees in tokens other than ETH** 3. Support for **cryptography** other than ECDSA: Schnorr, BLS, and beyond. 4. **Non-cryptographic modifications**, where users can require transactions to have expiry times, allows transactions to be confirmed slightly out-of-order and reducing inter-transaction dependence), and more. 5. Removal of relayers in **privacy solutions**. 6. **Sponsored transaction fees** whereby protocols pay user transaction fees. These 6 are only the beginning of use cases for account abstraction! As programmable transaction validity makes its way into production systems, we'll see even more use cases emerge. <ins>**Our first version of AA described below supports use cases 1, 2, and 3, but does not solve 4, 5, and 6. If you deeply care for 4-6, please reach out!**</ins> ## Specification All accounts are smart contracts and have some `code` attached to them. Default smart contract code will simulate EOA behavior. ### Transaction Flow 1. The operator checks that the user has enough funds to cover the transaction fee and charges the fee from the user. 2. The VM calls the `validateTransaction` method on the `from` smart contract and passes all the transaction data to it. During the call, the `from` account contract should validate the transaction’s information and return whether or not the transaction is valid. This step will have the following limitations to prevent DoS attacks: - It will have a limited amount of `ergs` (our equivalent to gas). - It will only be allowed to access its own storage (this is checked only on the server-side before submitting to the prover). 3. If the validation on step 2 failed, the operator will return all funds taken on step 1 and exit. Otherwise, the VM executes the transaction. Practically, this means executing the `to` contract with the provided transaction’s `data`. 4. The operator refunds the `from` account for the amount of ergs left after the execution. ### Transaction Status With AA, transactions can be in 3 possible states: - `Success`: executed, made state changes in both the verification and execution step of the tx - `Failed`: executed, paid the fee, and made changes only within the verification step, but didn’t make any state changes in the execution step - `FailedVerify`: failed AA verification step, no fee is paid and no state changes were made ### Transaction Type We will support 0-type Ethereum transactions as well as EIP-712 transactions. The EIP712 transaction format will be changed to have the `bytes` type of signature. It will have the following structure: ```solidity struct Transaction { uint8 txType; uint256 nonce; // It will also serve as ergsPrice limit uint256 gasPrice; // It will also serve as ergsLimit uint256 gasLimit; address to; uint256 value; bytes data; // Legacy fields, needed only for RLP transactions uint64 v; uint256 r; uint256 s; uint256 ergsLimitPerStorage; uint256 ergsLimitPerPubdata; uint256 feeToken; bytes signature; // inclusion is still under debate uint256 validFrom; uint256 validUntil; } ``` ## Wallets ### Interface Each account abstraction must implement the following method: ```javascript // Validates a transaction. The exact return value is tbd. function validateTransaction(Transaction transaction) external; ``` It should also implement the ERC-1271 signature verification standard: ```javascript function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4 magicValue); ``` Our SDK will use this method to validate signatures out of the box. ### L1→L2 Interaction Let’s say that we want to have an L2 AA controllable by an L1 smart contract. We can not provide the signature of the L1 smart contract, but what we can do is the following: - Set up the address of the L1 as the “master” of the L2 AA (the exact implementation is up to the implementation of each individual AA). - Request the execute from L1 with the `_fromAddressL2` equal to the L2 AA address and the rest of the params as the appropriate call params. From the perspective of the smart contract, the transaction will also require `bytes _signature`. For instance, here is the call for an execute on the L1 bridge: ```solidity function requestExecute( // The AA address address _fromAddressL2, // The `to` address for the execution step address _toAddressL2, // The `calldata` used for the execution step bytes memory _calldata, // The data used for authentication bytes memory _signature, // The args limit for the tx uint256 _ergsLimit, // The type of queue chosen for L1 --> L2 message Operations.QueueType _queueType, // The operation tree to use: Full or zkRollup Operations.OpTree _opTree ) external payable nonReentrant ``` The QueueType and OpTree parameters are further explained [here](https://aqwzx-v2-docs.zksync.dev/dev/zksync-v2/l1-l2-interop.html#priority-queue). The `_signature` field is needed in case the `_fromAddressL2` is a multisig controlled by multiple accounts and requires multiple signatures to conduct an operation. ### Upgradability Since the AA code is the same as the smart contract code, it won’t be upgradable by default. If users want to allow upgradability for their AA code, the `Proxy` pattern can be used. #### Case-study: change only the verification code and leave the contract code intact? What if the user only wants to modify the tx validation process, but not modify the rest of the functionality? The user can create a smart contract `Validator` that will implement a Proxy pattern and will be fully responsible for the tx validation process. The validation code of the main contract will look the following way: ```javascript function validateTransaction(Transaction tx) { (bool success, bytes memory data) = _validator.delegatecall( abi.encodeWithSignature("<passing data>") ); require(success); } ``` ### Default AA code At the genesis, all the contracts with address greater than `uint256(1023)` will have the default smart contract code assigned to them. Its main purpose is to emulate the behavior of the existing EOAs on Ethereum. It will implement all of the following two methods: ```jsx // Validates a transaction function validateTransaction(Transaction transaction) external; function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4 magicValue); ``` ## Architecture for DoS Prevention ### Problem Description Transaction execution under AA can be separated into two parts: 1. Validation: logic that decides whether execution should happen, such as nonce and signature checks. If this step fails, it is free. 2. Execution: transaction logic is executed. In Ethereum, transaction validation of signature, nonce, and value of EOAs can be done *in parallel*, and once added to the mempool, it remains valid with a guarantee of fee payment to the miner. In AA, the result of the validation of a transaction can be changed by the execution of a previous one, so the validation and execution steps must be done *sequentially* for the currently forming block. This is a major DoS threat: if millions of transactions with failed verification steps flood the system, all must be processed sequentially before processing transactions that actually change the state. To prevent this attack, we need to maximize the amount of “free” computation the operator can do in parallel. ### Limits on AA Verification To mitigate the DoS vector, the verification step will have the following limitations: 1. There is a hard-coded system global constant on the amount of `ergs` this step is allowed to spend. 2. This method only has access the storage of the wallet and can not access environment variables, such as `block.timestamp`. The first limitation will be so severe that privacy-preserving tooling will not be allowed. The second limitation will allow free verification to be done in parallel. ### Mempool Design The rules below do not apply to L1-originated transactions, which are included even if it does not pass the verification & fee payment steps. #### Transaction Inclusion To force sequential processing of `n` transactions’ verifications for free, the attacker needs to change at least `n/k` storage slots, where `k` is the max number of the simultaneous transactions from the same account in the mempool. If the transaction passes the preliminary validation by the read-only API node, it is accepted to the mempool. The mempool will have a limited size of n=5 transactions per address. Once in the mempool, the transaction can either: - Pass the validation and the operator will be paid for its sequential execution. - Be invalidated by a previously executed transactions and is deleted from the mempool. Since all new transactions are validated against the most recent state before getting accepted to the mempool and there can be at most 5 transactions with the same address in the mempool, this storage slot change could invalidate at most 5 transactions in the mempool. #### Transaction Modification All transactions keep the `nonce` field, for server purposes in managing the mempool. Mempool rules must allow for the ability of users to replace their transactions and the need to keep only a small constant amount of transactions originating from a single address. #### Rules 1. A transaction gets included in the mempool if and only if both of the following are true: - It has passed the verification & fee payment step. - It has `nonce` field equal to `last_nonce+k`, where `k <= 5`, where `last_nonce` is the `nonce` field of the last successfully executed transaction from this account. 2. If there is already a transaction with the same nonce, it gets overridden. 3. The next transaction chosen has a nonce of `last_nonce+1`. *The main issue with the provided rules is that it is hard to use with shared contracts, like Tornado Cash or Uniswap to pay the fees, because nonces provided in the txs by different users may interfere. Unfortunately, the alternative is not using nonces for transaction ordering and it is very inconvenient for Metamask / other wallet users. Even multisig users might get confused without nonce ordering. In this particular situation, we prioritize wallet/multisig users’ experience over protocol users’ experience. Any ideas are welcome.* #### Flow 1. The transaction is submitted to the read-only API node. If the transaction is invalid, does not have enough funds to pay the fee, or uses any storage slots not related to the account with the same address, the transaction is rejected. 2. Transaction gets added to the mempool according to the mempool rules. 3. Transaction is selected from the mempool to be executed. 4. If the transaction fails the verification step or to pay the fee, it is marked as having the `FailedVerify` state and it will not be added to the block and deleted from the mempool. If the verification step & the fee payment step succeed, the transaction is executed and applied to the state. It will be included in the next block. 5. Once the block is formed, it is sent to the cryptographic VM, which generates the proofs for the execution. ## Unsolved problems The following features were left out of the current AA design because they can be implemented without changes to our circuits. These are pure server-side changes which can be added but are non-trivial to implement. If you are passionate about these use cases, please reach out! ### Allow reading of immutable state of other contracts Reading the *immutable* state of other contracts during the verification step is safe but currently not allowed. ### Allow a larger verification gas limit for privacy applications Complex verifications such as SNARK verification is currently not possible under the current verification gas limit. ### Change mempool rules to support privacy applications Because current mempool rules use nonce to sort/override transactions, privacy-preserving applications (e.g. Tornado Cash) cannot work conveniently, because users’ supplied-nonces will interfere. We can support such applications by applying the following modifications to the mempool rules: - After a tx gets executed on an API node, the node remembers the list of all the storage slots, that were accessed (either read or write access) by the validation step. - Instead of having 5 txs per one address, we will allow no more than 5 txs per accessed storage slot. - The nonce rules are the same, e.g. if there are two transactions which access the same slot with the same `nonce` supplied as a part of the transaction, then the newest one overrides the previous one. ### Change mempool rules to support sponsored transactions by protocols or other parties Nonces provided by different users interfere with using shared contracts such as Uniswap to pay fees. The alternative is to not use nonces for transaction ordering, which presents an inconvenience for wallet users. In this situation, we have chosen to prioritize wallet/multisig users’ experience over protocol users’ experience. To support this, we could implement a mechanism similar to [paymasters](https://eips.ethereum.org/EIPS/eip-4337#extension-paymasters) detailed in EIP-4337. Paymasters do introduce an additional DoS vector, so we would introduce EIP-4337's [reputation scoring system](https://eips.ethereum.org/EIPS/eip-4337#reputation-scoring-and-throttlingbanning-for-paymasters) as well.