0/5
## Setting Up Your zkSync AA Test Environment with Foundry To begin testing your zkSync native Account Abstraction (AA) smart contracts, specifically a `ZkMinimalAccount`, we'll first establish the correct directory structure and initial test file using Foundry. This setup is crucial for organizing your tests and ensuring they interact correctly with your zkSync contracts. First, within your project's `test` directory, create a new folder named `zksync`. This convention helps separate zkSync-specific tests from tests for other chains or standard EVM contracts. Inside this newly created `test/zksync/` folder, create a new Solidity test file named `ZkMinimalAccountTest.t.sol`. This file will house the unit tests for our `ZkMinimalAccount` contract. Here's the initial boilerplate for `ZkMinimalAccountTest.t.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test} from "forge-std/Test.sol"; import {ZkMinimalAccount} from "src/zksync/ZkMinimalAccount.sol"; // Contract to be tested import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; // For test interactions contract ZkMinimalAccountTest is Test { ZkMinimalAccount minimalAccount; ERC20Mock usdc; uint256 constant AMOUNT = 1e18; // A standard amount for minting/transfers function setUp() public { minimalAccount = new ZkMinimalAccount(); // Directly instantiate for zkSync testing usdc = new ERC20Mock(); // Deploy a mock ERC20 token } // ... tests will go here } ``` In this setup: * We import `Test` from `forge-std` for Foundry's testing utilities. * `ZkMinimalAccount` is imported from `src/zksync/ZkMinimalAccount.sol`, representing the smart contract account we intend to test. * `ERC20Mock` from OpenZeppelin contracts is imported to simulate an ERC20 token (like USDC) for interaction during tests. * The `setUp()` function is a standard Foundry hook that runs before each test. Here, we initialize our `minimalAccount` by directly instantiating it using `new ZkMinimalAccount()`. This is a key point for zkSync native AA testing in Foundry, often simpler than the complex deployment scripts sometimes seen with ERC-4337 setups on Ethereum L1. We also deploy a `usdc` mock token. * A constant `AMOUNT` is defined for convenience in tests involving token transfers or minting. A quick note on naming consistency: ensuring your directory names (e.g., `zksync` in lowercase) are consistent between your `src` and `test` folders is good practice and can prevent import path issues. ## Key Differences: zkSync Native AA vs. Ethereum ERC-4337 Testing When testing Account Abstraction, it's vital to understand the distinctions between zkSync's native AA implementation and Ethereum L1's ERC-4337 standard. These differences significantly impact how you write and structure your tests. **Conditional Deployment:** While not implemented in this specific lesson for simplicity, real-world projects like the `minimal-account-abstraction` repository often use conditional logic for deployment. This typically involves a helper function (e.g., `isZkSyncChain()` from `foundry-devops`) to determine the current chain. * On **non-zkSync EVM chains**, deployment might involve a deployer script: `minimalAccount = deployer.deploy();`. * On **zkSync**, as shown in our `setUp()`, direct instantiation `minimalAccount = new ZkMinimalAccount();` is often sufficient and preferred for simpler test scenarios. The reason for bypassing more complex deployment scripts in this lesson is that zkSync's interaction with certain Foundry scripts and cheat codes can differ from standard EVM behavior, and direct instantiation keeps the focus on the core AA testing logic. **Transaction Execution Mechanics:** The most significant difference lies in how transactions are executed by the smart account: * **Ethereum ERC-4337 `MinimalAccount`:** Typically exposes an `execute(address dest, uint256 value, bytes memory funcData)` function. The owner (an Externally Owned Account, EOA) calls this function directly to instruct the smart contract wallet to perform an action (e.g., call another contract, transfer ETH). * **zkSync Native `ZkMinimalAccount`:** Implements a function like `executeTransaction(bytes32 _txHash, bytes32 _suggestedSignedHash, Transaction memory _transaction)`. This function is integral to zkSync's native AA protocol. * It's often invoked by the zkSync **bootloader** (a system-level contract) or directly by the account owner. * Crucially, it takes a `Transaction` struct as an argument. This struct is a zkSync-specific data structure that encapsulates all details of an operation: `to` (destination address), `value` (ETH amount), `data` (calldata), `nonce`, `gasLimit`, `gasPerPubdataByteLimit`, `factoryDeps`, `paymasterInput`, and `signature`. This provides a more comprehensive and structured way to define operations compared to the simpler `execute` function in ERC-4337. * The `ZkMinimalAccount.sol` contract itself (not detailed here but referenced in the video) would contain logic to handle these zkSync-specific transactions, which follow a lifecycle often referred to as Type 113 (or 0x71) transactions, involving distinct validation and execution phases. Understanding these distinctions is fundamental because your tests will need to simulate the correct transaction invocation pathway relevant to zkSync's native AA. ## Crafting Your First Test: `testZkOwnerCanExecuteCommands` Let's write our first test for the `ZkMinimalAccount`. The goal of `testZkOwnerCanExecuteCommands` is to verify that the owner of the `ZkMinimalAccount` can successfully instruct it to execute a command—specifically, to call the `mint` function on our mock `usdc` contract. This is analogous to testing owner-initiated actions in an Ethereum ERC-4337 AA wallet. Here's the initial structure of the test function, focusing on the "Arrange" phase: ```solidity function testZkOwnerCanExecuteCommands() public { // Arrange address dest = address(usdc); // The target contract is the mock USDC uint256 value = 0; // No ETH sent with the call itself // Calldata for usdc.mint(address(minimalAccount), AMOUNT) bytes memory functionData = abi.encodeWithSelector( ERC20Mock.mint.selector, address(minimalAccount), // Mint tokens to the minimalAccount AMOUNT // The amount defined as a constant ); // Act (This part will be different from Ethereum AA) // ... will involve constructing a Transaction struct and calling executeTransaction // Assert // ... will check if minimalAccount received the USDC tokens } ``` Let's break down the `Arrange` phase: 1. **`address dest = address(usdc);`**: We define the `dest` (destination) address as the address of our deployed `usdc` mock token contract. This is the contract our `minimalAccount` will interact with. 2. **`uint256 value = 0;`**: We set `value` to 0 because the intended operation is a token mint, which doesn't involve sending ETH alongside the call itself. 3. **`bytes memory functionData = abi.encodeWithSelector(...)`**: This is crucial. We are constructing the `calldata` for the operation the `minimalAccount` will perform. * `ERC20Mock.mint.selector`: This gets the 4-byte function selector for the `mint` function of our `ERC20Mock` contract. * `address(minimalAccount)`: This is the first argument to `mint`. We want the `minimalAccount` itself to be the recipient of the minted tokens. * `AMOUNT`: This is the second argument to `mint`, specifying how many tokens to mint. With these variables prepared, we have defined *what* action we want the `minimalAccount` to take. The next steps, the "Act" and "Assert" phases, will involve constructing the zkSync-specific `Transaction` struct and verifying the outcome. ## Core Concepts in zkSync AA Testing Several core concepts are essential when working with and testing zkSync's native Account Abstraction: * **Native Account Abstraction (zkSync):** Unlike Ethereum L1 where ERC-4337 is an application-level standard built on top of existing EOA infrastructure, zkSync has AA capabilities built directly into its protocol. This fundamental difference means transaction validation, execution, and fee payment mechanisms are handled differently and are more deeply integrated into the L2's architecture. * **The `Transaction` Struct (zkSync):** As highlighted earlier, this struct is a cornerstone of zkSync's AA. It bundles all necessary information for an operation (target, value, data, nonce, gas parameters, factory dependencies, paymaster details, and signatures) into a single, comprehensive object. Your tests will frequently involve constructing and manipulating these `Transaction` structs. * **The Bootloader (zkSync):** This is a special system contract on zkSync that plays a critical role in the transaction lifecycle. For AA accounts, the bootloader is often responsible for initiating the validation and execution phases, including potentially calling the `executeTransaction` (or similar) function on your smart account. * **Foundry Testing Specifics for zkSync:** While many standard Foundry testing primitives like `vm.prank`, assertions, and direct contract deployment (`new ContractName()`) work seamlessly, interacting with zkSync's unique AA features requires a deeper understanding of the zkSync protocol. This includes correctly formatting the `Transaction` struct and understanding how system contracts like the bootloader might influence transaction flow. * **Deployment Scripts (Bash):** For more complex deployment scenarios, especially those mimicking production environments or involving intricate factory dependencies on zkSync, using bash scripts that leverage `forge script` or `forge create` with zkSync-specific flags and RPC endpoints becomes necessary. AI tools like GitHub Copilot or ChatGPT can be helpful in scaffolding these scripts. * **Naming Consistency:** A minor but practical point is maintaining consistent naming conventions for directories (e.g., `zksync` vs. `zkSync`) between your source code (`src`) and test (`test`) folders to avoid potential import issues and maintain project clarity. ## Next Steps: Completing the `testZkOwnerCanExecuteCommands` Test The `testZkOwnerCanExecuteCommands` function is currently set up with its "Arrange" phase. The subsequent steps to complete this test, which were alluded to as the next part of the lesson, involve: 1. **Constructing the `Transaction` Struct:** * This involves populating a `Transaction` struct variable with the `to` (our `dest`), `value` (our `value`), and `data` (our `functionData`) prepared in the Arrange phase. * Additionally, other fields like `nonce` (which should be the current nonce of the `minimalAccount`), appropriate `gasLimit` and `gasPerPubdataByteLimit` for zkSync, and potentially empty `factoryDeps` and `paymasterInput` for this simple case, will need to be set. The `signature` field will also be crucial, representing the owner's authorization. 2. **Simulating the Transaction Execution (Act Phase):** * The owner of the `minimalAccount` needs to "submit" this transaction. In a Foundry test, this will likely involve: * Obtaining the owner's address (e.g., `minimalAccount.owner()`). * Using `vm.prank(ownerAddress)` to simulate the next call being made by the owner. * Calling `minimalAccount.executeTransaction(txHash, suggestedSignedHash, preparedTransactionStruct)`. The `txHash` and `suggestedSignedHash` would also need to be correctly computed or mocked according to zkSync's requirements. 3. **Asserting the Outcome (Assert Phase):** * After the `executeTransaction` call, we need to verify that the intended action occurred. * In this case, we would assert that the `usdc` token balance of the `minimalAccount` has increased by `AMOUNT`: `assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT);`. By completing these Act and Assert phases, you will have a full unit test verifying a core piece of functionality for your zkSync native `ZkMinimalAccount`.
A foundational guide to Setting Up Your zkSync AA Test Environment with Foundry - Learn to establish your Foundry project for zkSync native Account Abstraction tests, including directory structure, initial test files, and boilerplate for `ZkMinimalAccount`. This lesson also contrasts zkSync's native AA mechanics with Ethereum's ERC-4337.
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