0/5
## Building a Minimal Account Abstraction Contract on zkSync Era: System Calls and Simulations Welcome to this in-depth guide on constructing a minimal account abstraction contract, `ZkMinimalAccount.sol`, specifically tailored for the zkSync Era environment using Foundry. Our primary focus will be on mastering "Systems Contract Calls" and "zkSync Simulations"—unique mechanisms within zkSync for interacting with its core system contracts, particularly for managing account nonces. ## Project Setup and Contract Structure We begin by creating our contract file at `src/zksync/ZkMinimalAccount.sol`. This contract will implement the `IAccount` interface, which we import from `lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol`. To maintain clarity, we'll organize the contract using comments: ```solidity // src/zksync/ZkMinimalAccount.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import {IAccount} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol"; contract ZkMinimalAccount is IAccount { // EXTERNAL FUNCTIONS // INTERNAL FUNCTIONS } ``` This structure helps delineate between external functions required by the `IAccount` interface and any internal helper functions we might develop. ## The Core Logic: `validateTransaction` The `validateTransaction` function is paramount for our minimal account. In the zkSync context, its responsibilities are: 1. **Incrementing the Nonce:** A fundamental requirement for transaction validity in account abstraction. 2. **Validating the Transaction:** This involves: * Verifying that the account owner signed the transaction. * Ensuring the account possesses sufficient funds to cover transaction costs (as we are not implementing a Paymaster in this minimal example). 3. **Returning the Magic Value:** Consistent with ERC-4337 and standard Account Abstraction practices, it must return a specific `bytes4` magic value upon successful validation to indicate acceptance. ## Compiling for zkSync and Navigating Warnings Before compiling, ensure your Foundry toolchain is zkSync-compatible by running `foundryup-zksync`. To compile the contract for the zkSync Era environment, use the command: `forge build --zksync` You will likely encounter numerous compilation warnings. These are generally expected due to the differences between the zkEVM (used by zkSync Era) and the standard EVM. They do not share identical opcodes. Foundry cheat codes and common Ethereum libraries (like OpenZeppelin or Solmate) often use EVM-specific opcodes (e.g., `extcodesize`, `ecrecover`). These trigger warnings when compiled for zkSync because zkSync has native account abstraction, making `ecrecover` within the account contract less conventional, as accounts can support diverse signature schemes. **Important Tip:** Warnings originating from dependencies (e.g., `forge-std`, `openzeppelin-contracts`) can usually be disregarded, provided the warnings do not pertain directly to your custom contract code (`ZkMinimalAccount.sol`). ## Nonce Management in zkSync: The `NonceHolder` System Contract Unlike standard Ethereum Externally Owned Accounts (EOAs) where nonces are an implicit part of the account state, zkSync manages account nonces explicitly through a dedicated system contract: the `NonceHolder`. This contract, located at `lib/foundry-era-contracts/src/system-contracts/contracts/NonceHolder.sol`, is responsible for tracking and incrementing nonces for all accounts on the network. To increment an account's nonce, the account contract itself *must* interact with the `NonceHolder` system contract. The specific function we'll use within `NonceHolder` is `incrementMinNonceIfEquals`. This function ensures atomicity by only incrementing the nonce if the provided current nonce matches the one stored on-chain. ## The Challenge and Solution: System Contract Calls & zkSync Simulations Directly calling zkSync system contracts from other contracts can be complex and is often restricted for security reasons. zkSync addresses this with a mechanism called "simulations." These are specially crafted call patterns within your Solidity code that the zkSync compiler recognizes and transforms, *but only when a specific compiler flag is active*. When this flag is enabled, the compiler converts these simulation calls into the actual, low-level system contract calls required to interact with contracts like `NonceHolder`. If the flag is disabled, the simulation call remains as written, likely failing or behaving unexpectedly. Simulations thus serve as a developer-friendly abstraction layer, enabling privileged system contract interactions that are resolved at compile time. ## Activating Simulations: The `--system-mode` Compiler Flag To enable the zkSync compiler to process these simulations, you must use the `--system-mode=true` flag with your compilation command. **Crucial Correction:** While some older documentation or contexts might mention an `is-system = true` setting in `foundry.toml`, for Foundry zkSync projects, this is **incorrect**. The correct method is to pass the flag directly in the command line: ```bash forge build --zksync --system-mode=true ``` This flag instructs the zkSync compiler to recognize and transform simulation patterns into legitimate system calls. ## How zkSync Simulations Function: An Illustrative Example The core idea behind simulations is a specific syntax that the compiler, in "system mode," interprets specially. Consider this conceptual example (inspired by discussions on platforms like Ethereum Stack Exchange): ```solidity // Conceptual example of a simulation pattern // This is NOT actual production code for NonceHolder bool success = call(address(SYSTEM_CONTRACT_PLACEHOLDER), gasleft(), abi.encodeWithSelector(SOME_SELECTOR, some_argument)) == SystemContract.someFunction(expected_return_value); ``` * **Without `--system-mode=true`:** The compiler would treat this as a standard external call, comparing its boolean return value to the result of `SystemContract.someFunction(expected_return_value)`. * **With `--system-mode=true`:** The compiler recognizes this pattern. Instead of executing the `call` and the comparison, it replaces the *entire line* with the bytecode equivalent of making the intended system call, for example, `systemcontract.updateNonceHolder(1)` (if that were the target). The `call(...) == SystemContract...` syntax is effectively syntactic sugar. It's a pattern developers write, which the compiler, when in system mode, translates into the appropriate low-level system interaction. ## Implementing Nonce Incrementation via Simulations To implement the nonce increment, we'll leverage utilities provided by the `foundry-era-contracts` library, avoiding the need to write raw simulation patterns. 1. **Import `SystemContractsCaller`:** This library provides a helper function for making system calls. ```solidity import {SystemContractsCaller} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/SystemContractsCaller.sol"; ``` 2. **Import `NONCE_HOLDER_SYSTEM_CONTRACT` Address:** The address of the `NonceHolder` system contract is required. This is available in `Constants.sol`. ```solidity import {NONCE_HOLDER_SYSTEM_CONTRACT} from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol"; ``` *Note: While these addresses are generally stable for mainnet, they *can* change with network upgrades. The values in `Constants.sol` typically reflect the current mainnet deployment.* 3. **Import `INonceHolder` Interface:** To correctly ABI-encode the call data for our interaction with `NonceHolder`. ```solidity import {INonceHolder} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/INonceHolder.sol"; ``` 4. **Implement the Call in `validateTransaction`:** Within your `validateTransaction` function, after other checks but before returning the magic value, you will increment the nonce: ```solidity // Inside validateTransaction, after owner and fund checks // _transaction is the IAccount.Transaction struct passed to validateTransaction // This is the simulation: it gets replaced by a system call at compile time // when --system-mode=true is used. SystemContractsCaller.systemCallWithPropagatedRevert( uint32(gasleft()), // gasLimit: Pass remaining gas for the system call address(NONCE_HOLDER_SYSTEM_CONTRACT), // to: The NonceHolder system contract address 0, // value: No ETH value is sent for this particular system call abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce)) // data: Encoded call to NonceHolder.incrementMinNonceIfEquals with the expected current nonce ); ``` Let's break down the `systemCallWithPropagatedRevert` parameters: * `uint32(gasleft())`: Specifies the gas limit for the system call, using the remaining gas. * `address(NONCE_HOLDER_SYSTEM_CONTRACT)`: The target system contract address. * `0`: The Ether value sent with the call (zero in this case). * `abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))`: This is crucial. It ABI-encodes the call to the `incrementMinNonceIfEquals` function of the `INonceHolder` interface, passing the transaction's expected current nonce (`_transaction.nonce`) as the argument. This encoded data forms the payload for the system call. ## Essential Considerations and Best Practices * **Compiler Flag is Key:** Re-emphasizing, the `--system-mode=true` flag in your `forge build --zksync` command is non-negotiable for system contract interactions via simulations to work. * **Conceptual Complexity:** System calls and simulations can be initially confusing. If the exact mechanics aren't immediately clear, focus on the implementation pattern using `SystemContractsCaller`. Understanding will deepen with practice. * **Refer to Official Repositories:** For the most up-to-date and correct code, especially concerning system calls, flags, and dependency versions, always consult official or reference repositories like `Cyfrin/minimal-account-abstraction` (if this is the source of the lesson). * **Dependency Management:** Ensure you are using compatible versions of dependencies, such as `foundry-era-contracts`. Refer to project `Makefile` or `foundry.toml` files in reference implementations for correct versions. * **Verify System Contract Addresses:** You can use the zkSync Era block explorer to cross-reference addresses from `Constants.sol` (like `NONCE_HOLDER_SYSTEM_CONTRACT`) to confirm they correspond to the intended system contracts on the target network. By following these steps, you can effectively manage nonces in your zkSync account abstraction contracts. The process involves understanding the role of the `NonceHolder` system contract and leveraging zkSync's simulation mechanism, activated by the `--system-mode=true` compiler flag and facilitated by libraries like `SystemContractsCaller`. This allows your contract to securely and correctly interact with protected system functionalities.
A practical guide to Building a Minimal Account Abstraction Contract on zkSync Era: System Calls and Simulations - Construct `ZkMinimalAccount.sol` on zkSync Era, implementing the `validateTransaction` logic for nonce increments and signature checks. Navigate zkSync's unique "System Contract Calls" and "Simulations" using `SystemContractsCaller` and the `--system-mode` flag to manage nonces via `NonceHolder`.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025
Solidity Developer
Advanced FoundryDuration: 36min
Duration: 3h 06min
Duration: 5h 02min
Duration: 6h 02min
Duration: 2h 47min
Duration: 1h 23min
Duration: 4h 28min
Duration: 1h 19min
Duration: 1h 10min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Guest lecturers:
Juliette Chevalier
Lead Developer relations at Aragon
Nader Dabit
Director of developer relations at EigenLayer
Ally Haire
Developer relations at Protocol Labs
Harrison
Founder at GasliteGG
Last updated on May 12, 2025