# Important: Account abstraction support

# Introduction

On Ethereum, there are two types of accounts: externally owned accounts (EOAs) and smart contracts. The former type is the only one that can initiate transactions, while the latter is the only one that can implement arbitrary logic. For some use-cases, like smart-contract wallets or privacy protocols, this difference can creates a lot of friction. As a result they require L1 relayers, e.g. an EOA to help facilitate transactions from a smart-contract wallet.

Accounts in zkSync 2.0 can initiate transactions, like an EOA, but can also have arbitrary logic implemented in them, like a smart contract. This feature is called "account abstraction" and it is aimed to resolve the issues described above.

Unstable feature

This is the test release of account abstraction (AA) on zkSync 2.0. We are very happy to hear your feedback! Please note: breaking changes to the API/interfaces required for AA should be anticipated.

zkSync 2.0 is one of the first EVM-compatible chains to adopt AA, so this testnet is also used to see how "classical" projects from EVM chains can coexist with the account abstraction feature.

# Design

The account abstraction protocol on zkSync is in spirit very similar to EIP4337 (opens new window), though our protocol is still different for the sake of efficiency and better UX.

# IAccount interface

Each account is recommended to implement the IAccount (opens new window) 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 succeedes, 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.ergsLimit ETH to the bootloader address.
  • prePaymaster 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 the original 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 (opens new window) 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.ergsLimit to the operator. It should return the context that will be one of the call parameters to the postOp method.
  • postOp is optional and will be called after the transaction has been executed. Note, that unlike the original EIP4337, there is no guarantee that this method will be called. In particular, this method won't be called if the transaction has failed with out of gas error. It takes four parameters: the context returned by the validateAndPayForPaymasterTransaction method, the transaction itself, whether the execution of the transaction succeeded, and also the maximum amount of ergs the paymaster might be refunded with. More documentation on refunds will be available once their support is added to zkSync.

# Reserved fields of the Transaction struct with special meaning

Note that each of the methods above accepts the Transaction (opens new window) 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.

# The transaction flow

Each transaction goes through the following flow:

# The validation step

During the validation step, the account should decide whether it accepts the transaction and if so, pay the fees for it. If any part of the validation fails, the account is not charged a fee, and such transaction can not be included in a block.

Step 1. The system calls the validateTransaction method of the account. If it does not revert, proceed to the second step.

Step 2 (no paymaster). The system calls the payForTransaction method of the account. If it does not revert, proceed to the third step.

Step 2 (paymaster). The system calls the prePaymaster method of the sender. If this call does not revert, then the validateAndPayForPaymasterTransaction method of the paymaster is called. If it does not revert too, proceed to the third step.

Step 3. The system verifies that the bootloader has received at least tx.ergsPrice * tx.ergsLimit ETH to the bootloader. If it is the case, the verification is considered complete and we can proceed to the next step.

# The execution step

The execution step is considered responsible for the actual execution of the transaction and sending the refunds for any unused ergs back to the user. If there is any revert on this step, the transaction is still considered valid and will be included in the block.

Step 4. The system calls the executeTransaction method of the account.

Step 5. (only in case the transaction has a paymaster) The postOp method of the paymaster is called. This step should typically be used for refunding the sender the unused ergs in case the paymaster was used to facilitate paying fees in ERC-20 tokens.

# Fees

In EIP4337 you can see three types of gas limits: verificationGas, executionGas, preVerificationGas, that describe the gas limit for the different steps of the transaction inclusion in a block. zkSync has only a single field, ergsLimit, that covers the fee for all three. When submitting a transaction, make sure that ergsLimit is enough to cover verification, paying the fee (the ERC20 transfer mentioned above), and the actual execution itself.

By default, calling estimateGas adds a constant to cover charging the fee and the signature verification for EOA accounts.

# Extending EIP4337

To provide DoS protection for the operator, the original EIP4337 imposes several restrictions (opens new window) on the validation step of the account. Most of them, especially regarding the forbidden opcodes are still relevant. However, several restrictions have been lifted for better UX.

# Extending the allowed opcodes

  • It is allowed to call/delegateCall/staticcall contracts that were already deployed. Unlike Ethereum, we have no way to edit the code that was deployed or delete the contract via selfdestruct, so we can be sure that the code during the execution of the contract will be the same.

# Extending the set of slots that belong to a user

In the original EIP, the validateTransaction step of the AA allows the account to read only the storage slots of its own. However, there are slots that semantically belong to that user but are actually located on another contract’s addresses. A notable example is ERC20 balance.

This limitation provides DDoS safety by ensuring that the slots used for validation by various accounts do not overlap, so there is no need for them to actually belong to the account’s storage.

To enable reading the user's ERC20 balance or allowance on the validation step, the following types of slots will be allowed for an account with address A on the validation step:

  1. Slots that belong to address A.
  2. Slots A on any other address.
  3. Slots of type keccak256(A || X) on any other address. (to cover mapping(address => value), which is usually used for balance in ERC20 tokens.
  4. Slots of type keccak256(X || OWN) on any other address, where OWN is some slot of the previous (third) type (to cover mapping(address ⇒ mapping(address ⇒ uint256)) that are usually used for allowances in ERC20 tokens.

# What could be allowed in the future?

In the future, we might even allow time-bound transactions, e.g. allow checking that block.timestamp <= value if it returned false, etc. This would require deploying a separate library of such trusted methods, but it would greatly increase the capabilities of accounts.

# Building custom accounts

As already mentioned above, each account should implement the IAccount interface.

An example of the implementation of the AA interface is the implementation (opens new window) of the EOA account. Note that this account, just like standard EOA accounts on Ethereum, successfully returns empty value whenever it is called by an external address, while this may not be the behaviour that you would like for your account.

# EIP1271

If you are building a smart wallet, we also highly encourage you to implement the EIP1271 (opens new window) signature-validation scheme. This is the standard that is endorsed by the zkSync team. It is used in the signature-verification library described below in this section.

# The deployment process

The process of deploying account logic is very similar to the one of deploying a smart contract. In order to protect smart contracts that do not want to be treated as an account, a different method of the deployer system contract should be used to do it. Instead of using create/create2, you should use the createAccount/create2Account methods of the deployer system contract.

Here is an example of how to deploy account logic using the zksync-web3 SDK:

import { ContractFactory } from "zksync-web3";

const contractFactory = new ContractFactory(abi, bytecode, initiator, "createAccount");
const aa = await contractFactory.deploy(...args);
await aa.deployed();

# Limitations of the verification step

Not implemented yet

The verification rules are not fully enforced right now. Even if your custom account works right now, it might stop working in the future if it does not follow the rules below.

In order to protect the system from a DoS threat, the verification step must have the following limitations:

  • The account logic can only touch slots that belong to the account. Note, that the definition is far beyond just the slots that are at the users' address.
  • The account logic can not use context variables (e.g. block.number).
  • It is also required that your account increases the nonce by 1. This restriction is only needed to preserve transaction hash collision resistance. In the future, this requirement will be lifted to allow more generic use-cases (e.g. privacy protocols).

Transactions that violate the rules above will not be accepted by the API, though these requirements can not be enforced on the circuit/VM level and do not apply to L1->L2 transactions.

To let you try out the feature faster, we decided to release account abstraction publicly before fully implementing the limitations' checks for the verification step of the account. Currently, your transactions may pass through the API despite violating the requirements above, but soon this will be changed.

# Nonce holder contract

For optimization purposes, both tx nonce and the deployment nonce are put in one storage slot inside the NonceHolder system contracts. In order to increment the nonce of your account, it is highly recommended to call the incrementNonceIfEquals (opens new window) function and pass the value of the nonce provided in the transaction.

This is one of the whitelisted calls, where the account logic is allowed to call outside smart contracts.

# Sending transactions from an account

For now, only EIP712 transactions are supported. To submit a transaction from a specific account, you should provide the from field of the transaction as the address of the sender and the customSignature field of the customData with the signature for the account.

import { utils } from "zksync-web3";

// here the `tx` is a `TransactionRequest` object from `zksync-web3` SDK.
// and the zksyncProvider is the `Provider` object from `zksync-web3` SDK connected to zkSync network.
tx.from = aaAddress;
tx.customData = {
  ...tx.customData,
  customSignature: aaSignature,
};
const serializedTx = utils.serialize({ ...tx });

const sentTx = await zksyncProvider.sendTransaction(serializedTx);

# Paymasters

Paymasters are accounts that can compensate for other accounts' transactions. Imagine being able to pay fees for users of your protocol! The other important use-case of paymasters is to facilitate paying fees in ERC20 tokens. While ETH is the main token of zkSync, paymasters can provide the ability to exchange ERC20 tokens to ETH on the fly.

If users want to interact with a paymaster, they should provide the non-zero paymaster address in their EIP712 transaction. The input data to the paymaster is provided in the paymasterInput field of the paymaster.

# Paymaster verification rules

Not implemented yet

The verification rules are not fully enforced right now. Even if your paymaster works right now, it might stop working in the future if it does not follow the rules below.

Since multiple users should be allowed to access the same paymaster, malicious paymasters can do a DoS attack on our system. To work around this, a system similar to the EIP4337 reputation scoring (opens new window) will be used.

Unlike in the original EIP, paymasters are allowed to touch any storage slots. Also, the paymaster won't be throttled if either of the following is true:

  • More than X minutes have passed since the verification has passed on the API nodes. (The exact value of X will be defined later).
  • The order of slots being read is the same as during the run on the API node and the first slot whose value has changed is one of the user's slots. This is needed to protect the paymaster from malicious users (e.g. the user might have erased the allowance for the ERC20 token).

# Built-in paymaster flows

While some paymasters can trivially operate without any interaction from users (e.g. a protocol that always pays fees for their users), some require active participation from the transaction's sender. A notable example is a paymaster that swaps users' ERC20 tokens to ETH as it requires the user to set the necessary allowance to the paymaster.

The account abstraction protocol by itself is generic and allows both accounts and paymasters to implement arbitrary interactions. However, the code of default accounts (EOAs) is constant, but we still want them to be able to participate in the ecosystem of custom accounts and paymasters. That's why we have standardized the paymasterInput field of the transaction to cover the most common uses-cases of the paymaster feature.

Your accounts are free to implement or not implement the support for these flows. However, this is highly encouraged to keep the interface the same for both EOAs and custom accounts.

# General paymaster flow

It should be used if no prior actions are required from the user for the paymaster to operate.

The paymasterInput field must be encoded as a call to a function with the following interface:

function general(bytes calldata data);

EOA accounts will do nothing and the paymaster can interpret this data in any way.

# Approval-based paymaster flow

It should be used if the user is required to set certain allowance to a token for the paymaster to operate. The paymasterInput field must be encoded as a call to a function with the following signature:

function approvalBased(
    address _token, 
    uint256 _minAllowance, 
    bytes calldata _innerInput
)

The EOA will ensure that the allowance of the _token towards the paymaster is set to at least _minAllowance. The paymaster is free to interpret the _innerInput however it wants to.

If you are developing a paymaster, you should not trust the transaction sender to honestly behave (e.g. provide the required allowance with the approvalBased flow). These flows serve mostly as instructions to EOAs and the requirements should always be double-checked by the paymaster.

# Working with paymaster flows using zksync-web3 SDK

The zksync-web3 SDK provides methods for encoding correctly formatted paymaster params for all of the built-in paymaster flows.

# Testnet paymaster

To let users experience using with paymasters on testnet, as well as to keep supporting paying fees in ERC20 tokens, the Matter Labs team provides the testnet paymaster, that enables paying fees in ERC20 token at a 1:1 exchange rate with ETH (i.e. one unit of this token is equal to 1 wei of ETH).

The paymaster supports only the approval based paymaster flow and requires that the token param is equal to the token being swapped and minAllowance to equal to least tx.maxFeePerErg * tx.ergsLimit.

An example of how to use testnet paymaster can be seen in the hello world tutorial.

# aa-signature-checker

Your project can start preparing for native AA support. We highly encourage you to do so, since it will allow you to onboard hundreds of thousands of users (e.g. Argent users that already use the first version of zkSync). We expect that in the future even more users will switch to smart wallets.

One of the most notable differences between various types of accounts to be built is different signature schemes. We expect accounts to support the EIP-1271 (opens new window) standard. Our team has created a utility library for verifying account signatures. Currently, it only supports ECDSA signatures, but we will add support for EIP-1271 very soon as well.

The aa-signature-checker library provides a way to verify signatures for different account implementations. Currently it only supports verifying ECDSA signatures. Very soon we will add support for EIP-1271 as well.

We strongly encourage you to use this library whenever you need to check that a signature of an account is correct.

# Adding the library to your project:

yarn add @matterlabs/signature-checker

# Example of using the library

pragma solidity ^0.8.0;

import { SignatureChecker } from "@matterlabs/signature-checker/contracts/SignatureChecker.sol";

contract TestSignatureChecker {
    using SignatureChecker for address;

    function isValidSignature(
        address _address,
        bytes32 _hash,
        bytes memory _signature
    ) public pure returns (bool) {
        return _address.checkSignature(_hash, _signature);
    }
}

# Verifying AA signatures within our SDK

It is also not recommended to use ethers.js library to verify user signatures.

Our SDK provides two methods with its utils to verify the signature of an account:

export async function isMessageSignatureCorrect(address: string, message: ethers.Bytes | string, signature: SignatureLike): Promise<boolean>;

export async function isTypedDataSignatureCorrect(
  address: string,
  domain: TypedDataDomain,
  types: Record<string, Array<TypedDataField>>,
  value: Record<string, any>,
  signature: SignatureLike
): Promise<boolean>;

Currently these methods only support verifying ECDSA signatures, but very soon they will support EIP1271 signature verification as well.

Both of these methods return true or false depending on whether the message signature is correct.

Last Updated: 8/30/2022, 2:33:15 PM