1/5
## Understanding `validateTransaction` in zkSync Account Abstraction In the zkSync ecosystem, Account Abstraction (AA) revolutionizes how accounts operate by allowing smart contracts to function as user accounts. These smart contract accounts must implement specific functions to interact seamlessly with the zkSync protocol. One of the most critical functions in this paradigm is `validateTransaction`. The zkSync system, typically via the Bootloader or an API client acting on its behalf, invokes the `validateTransaction` function as the very first step in processing a transaction initiated by a smart contract account. Its primary responsibilities are to: 1. Verify that the account consents to the transaction, usually by checking a cryptographic signature. 2. Perform preliminary checks, such as ensuring the account has sufficient funds to cover potential transaction fees. 3. Update the account's nonce to prevent replay attacks. This lesson focuses on testing the `validateTransaction` function within a `ZkMinimalAccount` smart contract using the Foundry testing framework. Our goal is to ensure it correctly returns a specific magic value when a transaction is properly signed by the account's owner, thereby simulating the successful completion of the first validation phase in the zkSync AA transaction lifecycle. ## The Significance of `ACCOUNT_VALIDATION_SUCCESS_MAGIC` When `validateTransaction` successfully validates a transaction, it is mandated to return a predefined constant: `ACCOUNT_VALIDATION_SUCCESS_MAGIC`. This value is `bytes4(IAccount.validateTransaction.selector)`. Returning this specific `bytes4` value signals to the zkSync system (e.g., the Bootloader) that the account has acknowledged and approved the transaction, and the system can proceed with the next steps in the transaction lifecycle, such as fee handling and execution. If any other value is returned, or if the call reverts, the transaction is considered invalid by the system. ## Phase 1: Transaction Validation in the zkSync Lifecycle The `validateTransaction` function plays a pivotal role in the initial phase of a transaction's journey on zkSync. The sequence is generally as follows: 1. **Transaction Submission:** A user sends a transaction destined for their smart contract account to a zkSync API Client (e.g., a node). 2. **Nonce Check (Client-side):** The client performs an initial check of the transaction's nonce. 3. **`validateTransaction` Invocation:** The client (or Bootloader) calls `validateTransaction` on the target smart contract account. This call *must* update the account's nonce internally. 4. **Nonce Verification (Client-side):** The client checks if the nonce was indeed updated by the `validateTransaction` call. 5. **Fee Handling:** The client proceeds to handle fee payments, which might involve calling `payForTransaction` on the account or `prepareForPaymaster` if a paymaster is used. 6. **Bootloader Payment Verification:** The client ensures that the Bootloader, the system contract responsible for orchestrating transaction execution, will be compensated. Our test will focus on step 3, specifically ensuring the `validateTransaction` function behaves correctly when called by the Bootloader with a validly signed transaction. ## Configuring Foundry for zkSync Account Abstraction Testing Testing zkSync-specific functionalities, especially those involving system-level interactions like `validateTransaction`, requires a properly configured Foundry environment. Key elements include: * **Foundry Cheatcodes:** We leverage standard Foundry cheatcodes like `vm.prank` (to simulate calls from specific addresses), `vm.sign` (to generate signatures for testing), and `vm.deal` (to allocate ETH to accounts). * **zkSync Command-Line Flags:** * `--zksync`: This flag enables the zkSync compilation and execution environment within Foundry, allowing it to understand zkSync opcodes and system contracts. * `--system-mode=true`: This crucial flag permits tests to interact with zkSync system contracts. It effectively simulates the privileged environment in which system contracts, like the Bootloader, operate. This may supersede older configurations like `is-system = true` in `foundry.toml`. * **System Contracts & Bootloader:** zkSync relies on special smart contracts to manage protocol operations. The `BOOTLOADER_FORMAL_ADDRESS` is a constant representing the address of the Bootloader contract. When testing `validateTransaction`, we must simulate the call originating from this address. ## Building the `testZkValidateTransaction` Function We will construct our test case, `testZkValidateTransaction`, following the standard Arrange-Act-Assert pattern. This test will verify that `validateTransaction` returns `ACCOUNT_VALIDATION_SUCCESS_MAGIC` for a correctly signed transaction. **Helper Imports:** Before diving into the test, ensure you have the necessary imports in your test contract: ```solidity import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import {MemoryTransactionHelper} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol"; import {Transaction} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/TransactionHelper.sol"; import {BOOTLOADER_FORMAL_ADDRESS} from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol"; import {IAccount, ACCOUNT_VALIDATION_SUCCESS_MAGIC} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol"; // Constants used in the test bytes32 constant EMPTY_BYTES32 = bytes32(0); uint256 constant AMOUNT = 1 ether; // Example amount ``` **Test Function Structure:** ```solidity contract ZkMinimalAccountTest is Test { using MessageHashUtils for bytes32; ZkMinimalAccount minimalAccount; ERC20Mock usdc; address constant ANVIL_DEFAULT_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; // setUp function will be detailed later function testZkValidateTransaction() public { // Arrange address dest = address(usdc); uint256 value = 0; bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); // Create an unsigned zkSync Era transaction (type 113) // The _createUnsignedTransaction helper is assumed to be similar to previous lessons, // populating fields like nonce, from, to, gasLimit, gasPerPubdataByteLimit, etc. // For this lesson, `minimalAccount.owner()` is ANVIL_DEFAULT_ACCOUNT due to setUp. // The nonce should be the current expected nonce for the account. Transaction memory transaction = _createUnsignedTransaction(minimalAccount.owner(), 113, dest, value, functionData); // Sign the transaction using a new helper transaction = _signTransaction(transaction); // Act // Simulate the call originating from the Bootloader vm.prank(BOOTLOADER_FORMAL_ADDRESS); // The first two arguments (_txHash, _suggestedSignedHash) are passed as EMPTY_BYTES32 // as they are not central to this basic signature validation test. bytes4 magic = minimalAccount.validateTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction); // Assert assertEq(magic, ACCOUNT_VALIDATION_SUCCESS_MAGIC, "Validation did not return success magic"); } // _createUnsignedTransaction helper function (implementation details omitted for brevity, // focuses on populating the Transaction struct fields excluding the signature) function _createUnsignedTransaction( address _owner, // This would be transaction.from uint256 _txType, // e.g., 113 for ZkSync Era address _to, uint256 _value, bytes memory _data // ... other necessary params like nonce, gasLimit, gasPerPubdata, etc. ) internal view returns (Transaction memory unsignedTx) { unsignedTx = Transaction({ txType: _txType, from: uint256(uint160(_owner)), // In zkSync, `from` is uint256 to: uint256(uint160(_to)), // `to` is also uint256 gasLimit: 1000000, // Example value gasPerPubdataByteLimit: 800, // Example value maxFeePerGas: 100000000, // Example value (0.1 gwei) maxPriorityFeePerGas: 0, // Example value paymaster: 0, // No paymaster nonce: minimalAccount.getNonce(), // Crucial: get current nonce value: _value, reserved: [uint256(0), uint256(0), uint256(0), uint256(0)], // Reserved fields data: _data, signature: bytes(""), // Empty for unsigned factoryDeps: new bytes32[](0), // No factory dependencies paymasterInput: bytes(""), // Empty for no paymaster reservedDynamic: bytes("") // Empty }); // Ensure to correctly populate all fields as per zkSync transaction requirements } // _signTransaction helper function will be detailed next } ``` **Arrange Phase:** 1. Transaction details (`dest`, `value`, `functionData`) are defined, similar to a standard transaction call setup. 2. An unsigned `Transaction` struct is instantiated using the `_createUnsignedTransaction` helper. This helper populates all necessary fields of the zkSync `Transaction` struct *except* for the signature. It's crucial that `transaction.from` is set to the owner's address and `transaction.nonce` is the current expected nonce of the account. 3. The core new step in this test's arrangement is calling `_signTransaction(transaction)`. This helper (detailed below) calculates the transaction hash and uses `vm.sign` to generate a valid signature, which is then added to the transaction struct. **Act Phase:** 1. `vm.prank(BOOTLOADER_FORMAL_ADDRESS)`: This Foundry cheatcode is essential. It makes the subsequent call to `minimalAccount.validateTransaction` appear as if it's coming directly from the `BOOTLOADER_FORMAL_ADDRESS`. The `validateTransaction` function in `IAccount` typically requires `msg.sender == BOOTLOADER_FORMAL_ADDRESS`. 2. `minimalAccount.validateTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction)`: The function under test is called. The first two parameters, `_txHash` and `_suggestedSignedHash`, are related to EIP-712 typed data hashing and are not the primary focus for this basic signature validation test, so they are passed as `EMPTY_BYTES32`. The signed `transaction` struct is the key input. 3. The `bytes4` return value (the magic bytes) is captured in the `magic` variable. **Assert Phase:** 1. `assertEq(magic, ACCOUNT_VALIDATION_SUCCESS_MAGIC, "Validation did not return success magic")`: We assert that the `magic` value returned by `validateTransaction` is equal to the `ACCOUNT_VALIDATION_SUCCESS_MAGIC` constant. If they match, the test passes, confirming the function's correct behavior for a validly signed transaction. ## Implementing the `_signTransaction` Helper for Test Signatures To test `validateTransaction`, we need a way to sign transactions within our Foundry test environment. This is where the `_signTransaction` helper function comes in. It simulates how a wallet would sign a transaction, but uses Foundry's cheatcodes for local testing. ```solidity // (Inside ZkMinimalAccountTest contract) // Hardcoded default Anvil private key for testing uint256 constant ANVIL_DEFAULT_KEY = 0xac0974bec39a17e36ba46cd47b2cff49341e7a3373594e7397d7483645a9385; function _signTransaction(Transaction memory transaction) internal view returns (Transaction memory) { // 1. Encode the transaction hash for signing // MemoryTransactionHelper.encodeHash is specific to zkSync transaction structures bytes32 unsignedTransactionHash = MemoryTransactionHelper.encodeHash(transaction); // 2. Convert to Ethereum standard signed message hash format // This ensures compatibility with vm.sign, which expects an EIP-191 prefixed hash. bytes32 digest = unsignedTransactionHash.toEthSignedMessageHash(); // 3. Sign the digest using vm.sign and the known private key uint8 v; bytes32 r; bytes32 s; (v, r, s) = vm.sign(ANVIL_DEFAULT_KEY, digest); // 4. Create a mutable copy of the transaction to add the signature Transaction memory signedTransaction = transaction; // 5. Pack the signature components (r, s, v) into the signature field // The order r, s, v is a common convention. signedTransaction.signature = abi.encodePacked(r, s, v); return signedTransaction; } ``` **Explanation of `_signTransaction`:** 1. **Calculate Transaction Hash:** `MemoryTransactionHelper.encodeHash(transaction)` is used to compute the hash of the zkSync `Transaction` struct. This hash is specific to the zkSync transaction format. 2. **Convert to Ethereum Signed Message Hash:** `unsignedTransactionHash.toEthSignedMessageHash()` (from OpenZeppelin's `MessageHashUtils`) prepends the EIP-191 prefix (`\x19Ethereum Signed Message:\n32`) to the hash and then keccak256 hashes it again. This is the standard format expected by `vm.sign` and many wallet implementations. 3. **Sign with `vm.sign`:** * `ANVIL_DEFAULT_KEY`: This is the private key corresponding to `ANVIL_DEFAULT_ACCOUNT` (0xf39...). Foundry/Anvil provides a set of default accounts and their private keys for local development. * `vm.sign(ANVIL_DEFAULT_KEY, digest)`: This cheatcode takes the private key and the digest to be signed, returning the `v`, `r`, and `s` components of the ECDSA signature. 4. **Copy Transaction:** The input `transaction` is copied to `signedTransaction` to avoid modifying the original unsigned transaction struct if it's used elsewhere. 5. **Pack Signature:** `abi.encodePacked(r, s, v)` concatenates the `r`, `s`, and `v` components. This packed signature is then assigned to `signedTransaction.signature`. The order `r, s, v` is crucial. **Local vs. Live Network Signing:** It's important to differentiate this test signing mechanism from live network signing. In a local Foundry test, we know the private key (`ANVIL_DEFAULT_KEY`) and can directly use `vm.sign`. On a live network, the transaction signature would be provided by an external signer (e.g., a user's wallet), and the private key would not be exposed to the contract or the test environment. ## Essential `setUp` Function Adjustments for Validation Tests The `validateTransaction` function in `ZkMinimalAccount` (and typical AA contracts) internally verifies that the signature corresponds to the account's `owner()`. For our test to pass using `ANVIL_DEFAULT_KEY` for signing, the `minimalAccount` contract must be owned by `ANVIL_DEFAULT_ACCOUNT`. Additionally, the `validateTransaction` implementation within `ZkMinimalAccount` includes a check: `require(totalRequiredBalance <= address(this).balance, "ZkMinimalAccount_NotEnoughBalance")`. To satisfy this, the smart contract account needs an ETH balance. The `setUp` function is modified as follows: ```solidity // (Inside ZkMinimalAccountTest contract) function setUp() public { minimalAccount = new ZkMinimalAccount(); // Deploy the account contract // CRITICAL: Transfer ownership to the Anvil default account whose private key we use for signing. // This ensures that `signer == owner()` check passes inside the account's validation logic. minimalAccount.transferOwnership(ANVIL_DEFAULT_ACCOUNT); usdc = new ERC20Mock(); // Deploy a mock ERC20 token for transaction data // CRITICAL: Deal ETH to the minimalAccount. // The ZkMinimalAccount's validateTransaction checks if the account has enough balance // to cover potential fees (derived from transaction gas parameters). vm.deal(address(minimalAccount), AMOUNT); // AMOUNT can be 1 ether or any sufficient value } ``` These setup steps ensure that: 1. The signature generated by `_signTransaction` (using `ANVIL_DEFAULT_KEY`) will be valid because the `minimalAccount`'s owner is `ANVIL_DEFAULT_ACCOUNT`. 2. The balance check within `validateTransaction` will pass because the account has been funded with ETH using `vm.deal`. ## Executing Your zkSync Validation Test in Foundry To run this specific test for `validateTransaction` in a zkSync context, use the following Foundry command: ```bash forge test --mt testZkValidateTransaction --zksync --system-mode=true ``` * `--mt testZkValidateTransaction`: This flag (match test) tells Foundry to only run the test function named `testZkValidateTransaction`. * `--zksync`: This flag is essential. It instructs Foundry to compile and run the tests using the zkSync Era virtual machine and toolchain. * `--system-mode=true`: This flag enables "system mode," which is necessary for tests that interact with or simulate the behavior of zkSync system contracts, such as the Bootloader. Without this, calls requiring `msg.sender` to be a system address (like `BOOTLOADER_FORMAL_ADDRESS`) might fail. If the test setup, transaction signing, and `validateTransaction` logic are correct, the test should pass, confirming that your account contract correctly validates transactions signed by its owner. ## Key Considerations for Testing `validateTransaction` * **Caller Simulation (`vm.prank`):** Always use `vm.prank(BOOTLOADER_FORMAL_ADDRESS)` before calling `validateTransaction` in your tests to accurately simulate the zkSync system's invocation. * **Signer-Owner Alignment:** The private key used for signing in `_signTransaction` (e.g., `ANVIL_DEFAULT_KEY`) must correspond to the public address that owns the smart contract account (`ANVIL_DEFAULT_ACCOUNT` after `transferOwnership`). * **Account Balance (`vm.deal`):** If your `validateTransaction` logic (or any internal function it calls) checks for ETH balance (e.g., for pre-funding fee checks), ensure the account contract has sufficient balance using `vm.deal` in your test setup. * **Nonce Management:** `validateTransaction` is responsible for updating the account's nonce. While this specific test focuses on the return value, a more comprehensive test suite should also assert that the nonce is correctly incremented. * **zkSync Foundry Flags:** Remember to use `--zksync` and `--system-mode=true` (or the equivalent current flag for system interactions) when running your tests. * **Signature Packing Order:** The standard packing order for ECDSA signature components `v, r, s` in `abi.encodePacked` is `r, s, v`. Ensure your contract's signature recovery logic expects this order. * **Evolving Tooling:** The Foundry-zkSync integration is continually evolving (often in alpha or beta stages). Features, flags (like `--system-mode`), and best practices may change. Always refer to the latest official Foundry and zkSync documentation. Contributing feedback and reporting issues to the respective repositories is highly encouraged. By following these guidelines and understanding the intricacies of the `validateTransaction` flow, you can write robust Foundry tests for your zkSync Account Abstraction contracts, ensuring they integrate correctly with the zkSync protocol.
An essential guide to Validating zkSync AA Transactions in Foundry - Master testing the `validateTransaction` function for zkSync Account Abstraction contracts, focusing on signature verification and the `ACCOUNT_VALIDATION_SUCCESS_MAGIC` return. You'll configure Foundry with `--zksync` and `--system-mode`, use `vm.prank` for Bootloader simulation, and `vm.sign` for test signatures.
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