Introduction to native Account Abstraction in zkSync

Author: Written by ChiHaoLu, imToken Labs

This article mainly introduces the development and related content of abstract accounts in the zkSync Layer2 solution. The focus will be on three parts:

Table of Contents

  • Introduction

  • Overview of zkSync AA Contracts

  • Cost Models and LianGuaiymaster in the zkSync Era

  • Summary and Comparison

  • Conclusion

Background

  • Familiarity with smart contract wallets and their common features

  • A general understanding of how Ethereum transactions work

  • A general understanding of how EIP-4337 operates

  • A general understanding of how ZK (Validity) Rollup operates

  • Quick Look at zkSync

For the convenience of reading, it is not necessary to have a deep understanding of zkSync. Let’s briefly review some basic information about zkSync. zkSync has two main versions, version 1.0 (zkSync Lite) and version 2.0 (zkSync Era).

zkSync 1.0 only supports EOA (Externally Owned Account) and does not support smart contracts (only supports token transfers and exchanges). In contrast, zkSync 2.0, also known as zkSync Era, is native to AA (Abstract Account) (all account types are contracts, without EOA, which is the difference between EOA and contract accounts in Ethereum). It is also compatible with the EVM (Ethereum Virtual Machine) and supports the development of smart contracts using Rust, Yul, Vyper, Solidity, and other languages.

When referring to zkSync in the following text, it generally means zkSync 2.0, or zkSync Era.

In zkSync Era, there are also multiple System Contracts, which can be understood as implementing some important operating system functions of zkSync in smart contracts. These System Contracts are precompiled contracts that have never been deployed (they run directly on nodes), but they all have official addresses.

When executing the AA protocol, zkSync will perform logical operations and judgments through these System Contracts. For example, when verifying the nonce, it is determined by the NonceHolder, and the execution of the abstract account mechanism and the collection of transaction fees are determined by the bootloader. They will be introduced one by one in the following text.

Recap Account Abstraction

The core concept of account abstraction can be summarized into two key points: signature abstraction and payment abstraction.

The goal of signature abstraction is to enable various account contracts to use different verification schemes. This means that users are not limited to using digital signature algorithms with specific curves, but can choose any verification mechanism they prefer.

Payment abstraction aims to provide users with multiple transaction payment options. For example, they can use ERC-20 tokens for payment instead of native tokens, or transactions can be sponsored by third parties, or even more specific payment models.

Accounts in zkSync 2.0 can initiate transactions like EOA (Externally Owned Account), but they can also utilize their programmability to implement arbitrary logic, such as contract accounts. This is what we call Account Abstraction, which combines the advantages of two types of accounts in Ethereum, making the usage experience of AA (Account Abstraction) accounts more flexible, thereby achieving the two goals mentioned earlier: signature abstraction and payment abstraction.

AA Mechanism in zkSync Era

In the zkSync Era, the most important role of zkSync AA is the bootloader, which is a System Contract used for handling transactions and executing the AA mechanism, corresponding to the EntryPoint Contract of EIP-4337. The bootloader cannot be called by users (can only be triggered by the Operator) and has never been deployed (running directly on the node), but it has a formal address (used for receiving payments).

The Operator is an important role in ZK Rollup, a centralized Off-Chain Server, similar to the Sequencer that you may have seen, responsible for triggering the bootloader and other System Contracts externally.

The native account abstraction protocols (such as StarkNet and zkSync) are basically designed based on EIP-4337. In the implementation of zkSync, users send transactions to the Operator, who then sends the transactions to the bootloader and starts a series of processing.

From the perspective of a block:

When the bootloader receives input from the Operator, it first defines some environmental variables for the block (such as gas price, block number, block timestamp, etc.). Then, the bootloader sequentially reads the list of transactions, first queries whether the account contract agrees to the transaction (i.e., calling the validate function in the AA mechanism), and then puts them into the block.

After each transaction is verified, the Operator verifies whether the block is large enough to be sent to the validators (or whether it has timed out). If it is large enough or has timed out, the Operator closes the block, stops adding new transactions to the bootloader, and completes the transaction execution.

From the perspective of a transaction, when the Operator triggers the bootloader, the bootloader processes each transaction sequentially:

  1. Confirm whether the nonce corresponding to the user account contract address is valid.

  2. Call the validate function on the user account contract for validation.

  3. After the validation is passed, the account contract transfers the gas fee to the bootloader’s address (or through LianGuaiymaster, which will be introduced later), and the bootloader checks whether it has received enough funds.

  4. Call the execute function on the user account contract to execute the transaction.

The first three steps correspond to the verification loop of EIP-4337, and the fourth step corresponds to the execution loop of EIP-4337.

This is mainly an overview, and the details and roles of each step will be explained in detail in the following sections.

zkSync Abstract Account Contract Quick Overview

Nonce

The account nonce in zkSync is recorded in a system contract called NonceHolder, which remembers whether each (account_address, nonce) pair has been used or not through a mapping, in order to determine the validity of the nonce.

As mentioned earlier, the first step after the Operator triggers the bootloader is to check the nonce. Therefore, before each transaction begins, the NonceHolder will be used to verify whether the current nonce being used is valid (currently only checks if it has been used). If the nonce is valid, it will enter the verification phase, and the nonce will be marked as used; if it is invalid, the transaction (verification) will fail.

The key points about the current nonce of zkSync:

Although the current user can execute multiple transactions with different nonces to the account simultaneously, zkSync does not support parallel processing, so transactions with different nonces will still be processed in order.

In theory, users can use any non-zero 256-bit integer as the nonce, but zkSync still recommends using incrementNonceIfEquals as the way to manage the nonce to ensure that it increases in order (currently, zkSync’s AA mechanism only confirms unused nonces, but the official documentation indicates that future requirements may include sequential incrementation).

Account Contract

There are four necessary entry points in the account contract in zkSync, which are:

  • validateTransaction: Called during the validation phase to confirm whether the operation is authorized by the account owner. Users can customize their own validation logic here (such as various signature algorithms, multi-signature, etc.).

  • LianGuaiyForTransaction: When the transaction fee is paid by the account (not using LianGuaiymaster), the operator will call this function to pay at least tx.gasprice * tx.gaslimit ETH to the bootloader address.

  • preLianGuaireForLianGuaiymaster: When the transaction fee will be paid by LianGuaiymaster, the operator will call this function to prepare for the interaction with LianGuaiymaster. The example provided by zkSync is to approve the ERC-20 token of LianGuaiymaster.

  • executeTransaction: After successfully passing the validation phase and successfully charging the fee, this function will be used to execute the operation that the user wants to achieve (such as interacting with contracts, remittances, etc.).

Details about LianGuaiymaster, the amount of transaction fees (tx.gasprice * tx.gaslimit), etc. will be explained in subsequent chapters.

There is also an optional insurance function in the zkSync account called executeTransactionFromOutside. When the operation cannot be executed (such as no response from the sequence generator or the discovery of regulatory risks in zkSync), the “escape mechanism” can be used to withdraw funds to L1. This part is not closely related to the AA protocol, so it will not be described in detail here. Interested parties can refer to the official documentation and specifications of zkSync.

Key points and limitations of the validation function

In the validateTransaction function, various custom logics can be implemented. For example, if the account has implemented the EIP-1271 standard, the validation logic in EIP-1271 can be directly applied to validateTransaction, or refer to the implementation of multi-signature account contracts in zkSync’s official documentation.

At the same time, in the Verification Phase of EIP-4337, there are some limitations (such as not involving external opcodes and limited depth) to avoid DoS threats. Similar limitations also exist in zkSync, for example:

1. Contract logic can only touch its own slots (if the address of the account contract is A):

– Slots belonging to address A

– Slots of any other address A

– Slots keccak256(A||X) of any other address, which can directly use the address as the key of the mapping (such as mapping(address=>value)), and is also equivalent to allowing access to slots keccak256(A||X) to achieve extension. For example, token balances on ERC-20.

2. Contract logic should not use global variables, such as block.number

Key Points and Limitations of Execution Functions

In the executeTransaction function, it is important to note that if a system call needs to be executed, the isSystem flag must be ensured. Because these system contracts have a significant impact on the account system, for example, the only way to increase the nonce is to interact with NonceHolder, and to deploy a contract, it must interact with ContractDeployer. Using the isSystem flag can ensure that the account developer consciously interacts with the system contracts.

However, it is recommended to use the SystemContractsCaller library provided by zkSync during implementation to avoid handling the isSystem flag by yourself, and use the systemCallWithProLianGuaigatedRevert function in it to complete system calls.

The above code example involves interaction with `DEPLOYER_SYSTEM_CONTRACT`. The most common situation where account developers encounter system contracts is when they want to use an account to deploy a contract, in which case they must interact with the `ContractDeployer` system contract. In this case, account developers need to communicate with the `ContractDeployer` contract to ensure successful contract deployment and perform the required operations.

Fee Model and LianGuaiymaster in the Era of zkSync

Fees and Gas Limit

The fee model of zkSync is very similar to Ethereum, and the fee token is still ETH. However, in addition to the basic cost of computation and writing slots, like other Layer 2 solutions (such as Arbitrum and Optimism), zkSync also needs to consider the additional cost (security fee) of publishing to L1. Since the gas price for publishing data to L1 is very unstable, when each block is opened (transaction recording begins), the Operator of zkSync will define the following dynamic parameters:

gasPrice: The gas price in gwei, which is the tx.gasprice mentioned earlier in the transaction object

gasPerPubdata: The amount of gas required to publish one byte of data on Ethereum

In addition, unlike EIP-4337, zkSync does not need to define three gas limits: verificationGas, executionGas, and preVerificationGas. It only needs one gasLimit to cover all the cost, including the cost of the verification phase, execution phase, and the security fee of uploading data to L1. This cost is included in the tx.gaslimit mentioned in the transaction object.

Multiplying these two (tx.gasprice * tx.gaslimit) will give the amount of transaction fees paid to the bootloader.

LianGuaiymaster

LianGuaiymaster mainly replaces the user’s account contract to pay ETH to the bootloader during the transaction fee payment phase. Users can choose different LianGuaiymaster and payment methods to pay the fees, for example (but not limited to):

– Paying ERC-20 tokens to LianGuaiymaster before or after the transaction

– Recharging LianGuaiymaster contract with a credit card

– LianGuaiymaster will continue to pay part or all of the fees for users for free

The way users interact with LianGuaiymaster depends on different protocols, which can be centralized or decentralized; it can be before or after the transaction; it can use ERC-20 tokens or fiat currency, or even be free.

The LianGuaiymaster contract in zkSync consists of two main functions, namely validateAndLianGuaiyForLianGuaiymasterTransaction (required) and postTransaction (optional), both of which can only be called by the bootloader:

– validateAndLianGuaiyForLianGuaiymasterTransaction is the only function that must be implemented in the LianGuaiymaster contract. When the operator receives a transaction with LianGuaiymaster parameters, it indicates that the transaction fee is not paid by the user’s account contract, but by LianGuaiymaster. At this time, the operator will call validateAndLianGuaiyForLianGuaiymasterTransaction to determine if LianGuaiymaster is willing to pay the transaction fee. If LianGuaiymaster agrees, this function will send at least tx.gasprice * tx.gaslimit ETH to the bootloader.

– postTransaction is an optional function, usually used for refunds (returning unused gas to the sender). However, the current zkSync does not support this operation.

The LianGuaiymaster in zkSync will execute postTransaction only after implementing postTransaction, which is different from EIP-4337. In EIP-4337, postOp will not be called if validateLianGuaiymasterUserOp does not return the context, and vice versa.

To summarize, if a user wants to send a transaction with the transaction fee paid by LianGuaiymaster, the process is as follows:

  1. Confirm the legality of the nonce through NonceHolder.

  2. Call validateTransaction on the user’s Account Contract to verify that the transaction is authorized by the account owner.

  3. Call preLianGuaireForLianGuaiymaster on the user’s Account Contract, which may execute actions such as approving a certain amount of ERC-20 Tokens for LianGuaiymaster or do nothing.

  4. Call validateAndLianGuaiyForLianGuaiymasterTransaction on the LianGuaiymaster Contract to confirm that LianGuaiymaster is willing to pay and collect the transaction fee, while LianGuaiymaster charges the user a certain amount of ERC-20 tokens (previously approved).

  5. Confirm that the bootloader receives the correct amount of ETH fee (at least tx.gasprice * tx.gaslimit).

  6. Call executeTransaction on the user’s Account Contract to perform the desired transaction.

  7. If the LianGuaiymaster Contract has implemented postTransaction and there is still enough gas (no out of gas error), then execute postTransaction.

Even if the postTransaction cannot be executed due to an out of gas error in the final step, this AA transaction is still considered successful, with the omission of calling the postTransaction action.

For a deeper understanding of zkSync’s LianGuaiymaster, it can be found that its Verification Rules and 4337 are slightly different (zkSync LianGuaiymaster can step on any other contract’s slot), and there are also various types (such as Approval-based). Interested individuals can refer to the official documentation or my previous implementation for more detailed information.

Summary & Comparison

Through the explanation above, we have learned about the important entry points of the account contract, their functions, and related limitations. At the same time, we have also learned about the functionality of system contracts. Next, let’s summarize the process of an automatic operation (AA) transaction in zkSync from construction to completion, and I will also provide more detailed reference materials for those who want to delve deeper into it.

1. Users use SDK or wallets locally to build transaction objects (e.g., from, to, data, value, etc.).

2. Users sign the transaction. The signature here is not necessarily in the traditional EIP-712 format and ECDSA curve signature. zkSync also supports EIP-2718 and EIP-1559. The key to selecting the signature and verification method is to verify through the verification function in the account contract.

3. The signed transaction is sent to the operator (Operator) through the RPC API. At this time, the transaction enters the pending state. The operator passes the transaction to the bootloader (calls the processL2Tx function on the bootloader contract) and starts a series of AA protocol processes.

4. The bootloader checks whether the Nonce is valid using NonceHolder.

5. The bootloader calls the validateTransaction function on the user account contract to confirm that the transaction has been authorized by the account owner.

6. The bootloader collects fees in two ways, depending on the transaction parameters (whether the LianGuaiymaster parameter is included when building the transaction object):

a. Call the LianGuaiyForTransaction function to collect fees with the account contract;

b. Call the preLianGuaireForLianGuaiymaster and validateAndLianGuaiyForLianGuaiymasterTransaction functions to collect fees with the LianGuaiymaster contract.

7. “Call LianGuaiyForTransaction to pay fees to the Account contract” or “call preLianGuaireForLianGuaiymaster and validateAndLianGuaiyForLianGuaiymasterTransaction to pay fees to the LianGuaiymaster contract”

8. Check if the bootloader has received at least tx.gasprice * tx.gaslimit transaction fees.

9. The bootloader calls the executeTransaction function on the user account contract to execute the transaction.

10. (Optional) If LianGuaiymaster is used to pay fees, the bootloader calls the postTransaction function. If LianGuaiymaster has not implemented postTransaction or if the gas is exhausted, this step will be skipped.

Steps 4-7 are the verification stage (defined in l2TxValidation in the bootloader), and steps 8-9 are the execution stage (defined in l2TxExecution in the bootloader).

Comparison of EIP-4337, StarkNet, and zkSync Era

Basically, the AA mechanism processes of these three are similar, which are the verification stage → fee mechanism (paid by the account contract or LianGuaiymaster) → execution stage. The main differences are:

  • The roles of executing the AA mechanism: In the zkSync era, the difference between Operator and the other two AAs is that Operator needs to cooperate with the bootloader (system contract), for example, the bootloader will start a new block and define the relevant parameters of the block, receive transactions sent by the operator, and perform verification. In EIP-4337, this part is done by Bundler and EntryPoint, while in StarkNet, this part is entirely handled by Sequencer.

  • Whether Gas Cost needs to consider L1 security fees: L2 AAs need to consider the cost of uploading data to L1, not just the ZK (Validity) Rollups Native AA mentioned above. When implementing 4337 on Optimistic Rollups, L1 security fees also need to be taken into account (calculated in preVerificationGas, details can be found in relevant Alchemy files).

  • Whether transactions can be sent before deploying the account contract: In the StarkNet and zkSync eras, there is no initCode field in EntryPoint, as in EIP-4337, which allows sending transactions before deploying the account contract. Therefore, transactions cannot be sent before configuring the account.

Comparison

Due to the fact that StarkNet has not yet implemented the LianGuaiymaster mechanism and zkSync has not yet completed the design of the gas refund mechanism, some detailed comparisons are not listed here.

In addition, the current 4337 bundler has completed the P2P mempool, and the Sequencer and Operator of zkRollups are still the only official servers, so there is a certain degree of centralization.

In terms of development process, zkSync is easier to use because it does not have the issue of integrating with various bundlers (it only needs to interact with the Operator API), and the experience of developing account contracts (SDK) is also better; at the same time, zkSync can use Solidity as the contract development language, so there is no need to cross the threshold of Cairo in StarkNet development.

Conclusion

Since both StarkNet and zkSync belong to the scope of Native AA (Native Account Abstraction), you can also refer to the article I previously wrote about StarkNet AA, titled “Introduction of StarkNet Account Abstraction”. In addition, you can also read other articles related to EIP-4337 for more relevant information.

Like what you're reading? Subscribe to our top stories.

We will continue to update Gambling Chain; if you have any questions or suggestions, please contact us!

Follow us on Twitter, Facebook, YouTube, and TikTok.

Share:

Was this article helpful?

93 out of 132 found this helpful

Gambling Chain Logo
Industry
Digital Asset Investment
Location
Real world, Metaverse and Network.
Goals
Build Daos that bring Decentralized finance to more and more persons Who love Web3.
Type
Website and other Media Daos

Products used

GC Wallet

Send targeted currencies to the right people at the right time.