Design

Overview of ZKsync's account abstraction design, focusing on enhancing transaction efficiency and user experience.

The account abstraction protocol on ZKsync is very similar to EIP4337, though our protocol is still different for the sake of efficiency and better UX.

Keeping nonces unique

  • The current model does not allow custom wallets to send multiple transactions at the same time and maintain deterministic ordering.
  • For EOAs, nonces are expected to grow sequentially; while for custom accounts the order of transactions cannot be guaranteed.
  • In the future, we plan to switch to a model where accounts can choose between sequential or arbitrary nonce-ordering.

One of the important invariants of every blockchain is that each transaction has a unique hash. Holding this property with an arbitrary account abstraction is not trivial, though accounts can, in general, accept multiple identical transactions. Even though these transactions would be technically valid by the rules of the blockchain, violating hash uniqueness would be very hard for indexers and other tools to process.

There needs to be a solution on the protocol level that is both cheap for users and robust in case of a malicious operator. One of the easiest ways to ensure that transaction hashes do not repeat is to have a pair (sender, nonce) always unique.

The following protocol is used:

  • Before each transaction starts, the system queries the NonceHolder to check whether the provided nonce has already been used or not.
  • If the nonce has not been used yet, the transaction validation is run. The provided nonce is expected to be marked as "used" during this time.
  • After the validation, the system checks whether this nonce is now marked as used.

Users will be allowed to use any 256-bit number as nonce and they can put any non-zero value under the corresponding key in the system contract. This is already supported by the protocol, but not on the server side.

More documentation on various interactions with the NonceHolder system contract as well as tutorials will be available once support on the server side is released. For now, it is recommended to only use the incrementMinNonceIfEquals method, which practically enforces the sequential ordering of nonces.

Standardizing transaction hashes

In the future, it is planned to support efficient proofs of transaction inclusion on ZKsync. This would require us to calculate the transaction's hash in the bootloader. Since these calculations won't be free to the user, it is only fair to include the transaction's hash in the interface of the AA methods (in case the accounts may need this value for some reason). That's why all the methods of the IAccount and IPaymaster interfaces, which are described below, contain the hash of the transaction as well as the recommended signed digest (the digest that is signed by EOAs for this transaction).

Interfaces

IAccount interface

Each account is recommended to implement the IAccount interface.

It contains the following five methods:

  • validateTransaction is mandatory and will be used by the system to determine if the AA logic agrees to proceed with the transaction. In case the transaction is not accepted (e.g. the signature is wrong) the method should revert. In case the call to this method succeeds, the implemented account logic is considered to accept the transaction, and the system will proceed with the transaction flow.
  • executeTransaction is mandatory and will be called by the system after the fee is charged from the user. This function should perform the execution of the transaction.
  • payForTransaction is optional and will be called by the system if the transaction has no paymaster, i.e. the account is willing to pay for the transaction. This method should be used to pay for the fees by the account. Note, that if your account will never pay any fees and will always rely on the paymaster feature, you don't have to implement this method. This method must send at least tx.gasprice * tx.gasLimit ETH to the bootloader address.
  • prepareForPaymaster is optional and will be called by the system if the transaction has a paymaster, i.e. there is a different address that pays the transaction fees for the user. This method should be used to prepare for the interaction with the paymaster. One of the notable examples where it can be helpful is to approve the ERC-20 tokens for the paymaster.
  • executeTransactionFromOutside, technically, is not mandatory, but it is highly encouraged, since there needs to be some way, in case of priority mode (e.g. if the operator is unresponsive), to be able to start transactions from your account from "outside" (basically this is the fallback to the standard Ethereum approach, where an EOA starts transaction from your smart contract).

IPaymaster interface

Like in EIP4337, our account abstraction protocol supports paymasters: accounts that can compensate for other accounts' transactions execution. You can read more about them here.

Each paymaster should implement the IPaymaster interface.

It contains the following two methods:

  • validateAndPayForPaymasterTransaction is mandatory and will be used by the system to determine if the paymaster approves paying for this transaction. If the paymaster is willing to pay for the transaction, this method must send at least tx.gasprice * tx.gasLimit to the operator. It should return the context that will be one of the call parameters to the postTransaction method.
  • postTransaction is optional and is called after the transaction executes. Note that unlike EIP4337, there is no guarantee that this method will be called. In particular, this method won't be called if the transaction fails with out of gas error. It takes four parameters:
    • the context returned by validateAndPayForPaymasterTransaction,
    • the transaction itself,
    • a flag that indicates whether the transaction execution succeeded,
    • the maximum amount of gas the paymaster might be refunded with

Reserved fields of the Transaction struct

Note that each of the methods above accept the Transaction struct.

While some of its fields are self-explanatory, there are also 6 reserved fields, the meaning of each is defined by the transaction's type. We decided to not give these fields names, since they might be unneeded in some future transaction types. For now, the convention is:

  • reserved[0] is the nonce.
  • reserved[1] is msg.value that should be passed with the transaction.

Transaction Flow

Each transaction is processed through the following stages:

Validation

During validation, the system determines if the transaction is acceptable. If the transaction fails at any validation point, no fees are charged to the account, and the transaction cannot be included in a block.

Steps in the Validation Process

  1. Nonce Verification: The system verifies that the transaction's nonce has not been previously used. Further details on maintaining nonce uniqueness can be found here.
  2. Transaction Validation: The validateTransaction method on the account is invoked. If this method executes successfully without reverting, the process moves to the next step.
  3. Nonce Marking: Post-validation, the system marks the nonce of the transaction as used.
  4. Fee Handling:
    • Standard Transactions: The payForTransaction method is called on the account. If this method does not revert, the transaction proceeds.
    • Paymaster Transactions: Initially, the prepareForPaymaster method is called on the sender. If successful, it is followed by the validateAndPayForPaymasterTransaction method on the paymaster. If neither method reverts, the process moves forward.
  5. Funds Verification: The system ensures that the bootloader has received at least tx.gasPrice * tx.gasLimit ETH. If the required funds are present, the transaction is deemed verified and is ready for the next step.

Execution

The execution step is responsible for performing the actual transaction operations and refunding any unused gas to the user. Even if this step results in a revert, the transaction remains valid and is included in the block.

Steps in the Execution Process

  1. Execute Transaction: The executeTransaction method on the account is called to carry out the transaction.
  2. Paymaster Post-Transaction Handling (Applicable only if a paymaster is involved): The postTransaction method on the paymaster is invoked. This method typically handles the refund of unused gas to the sender, especially in scenarios where the paymaster facilitates fee payment in ERC-20 tokens.

Fees

The handling of transaction fees varies between different protocols, as illustrated by EIP-4337 and ZKsync Era.

Gas Limits in EIP-4337

EIP-4337 defines three types of gas limits to manage the costs associated with different transaction stages:

  • verificationGas: Covers the gas required for transaction verification.
  • executionGas: Allocates gas for the execution of the transaction.
  • preVerificationGas: Specifies the gas used prior to the main verification process.

Unified Gas Limit in ZKsync Era

Contrastingly, ZKsync Era simplifies the fee structure by using a single gasLimit field for all transaction-related costs. This unified gasLimit must be adequately set to cover:

  • Verification of the transaction.
  • Payment of the fee, including any ERC-20 transfers.
  • Execution of the transaction itself.

Estimating Gas

By default, the estimateGas function calculates the required gas amount and includes an additional constant. This constant accounts for fee payment and signature verification for Externally Owned Account (EOA) transactions.

Using the SystemContractsCaller library

For the sake of security, both NonceHolder and the ContractDeployer system contracts can only be called with a special isSystem flag. You can read more about it here.

To make a call with this flag, the systemCall/systemCallWithPropagatedRevert/systemCallWithReturndata methods of the SystemContractsCaller library should be used.

Using this library is practically a must when developing custom accounts since this is the only way to call non-view methods of the NonceHolder system contract. Also, you will have to use this library if you want to allow users to deploy contracts of their own. You can use the implementation of the EOA account as a reference.


Made with ❤️ by the ZKsync Community