Design
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 leasttx.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 leasttx.gasprice * tx.gasLimit
to the operator. It should return thecontext
that will be one of the call parameters to thepostTransaction
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 without 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
- the context returned by
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]
ismsg.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
- 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.
- Transaction Validation: The
validateTransaction
method on the account is invoked. If this method executes successfully without reverting, the process moves to the next step. - Nonce Marking: Post-validation, the system marks the nonce of the transaction as used.
- 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 thevalidateAndPayForPaymasterTransaction
method on the paymaster. If neither method reverts, the process moves forward.
- Standard Transactions: The
- 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
- Execute Transaction: The
executeTransaction
method on the account is called to carry out the transaction. - 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.