5/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.
validateTransaction
in zkSync Account AbstractionIn 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:
Verify that the account consents to the transaction, usually by checking a cryptographic signature.
Perform preliminary checks, such as ensuring the account has sufficient funds to cover potential transaction fees.
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.
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.
The validateTransaction
function plays a pivotal role in the initial phase of a transaction's journey on zkSync. The sequence is generally as follows:
Transaction Submission: A user sends a transaction destined for their smart contract account to a zkSync API Client (e.g., a node).
Nonce Check (Client-side): The client performs an initial check of the transaction's nonce.
validateTransaction
Invocation: The client (or Bootloader) calls validateTransaction
on the target smart contract account. This call must update the account's nonce internally.
Nonce Verification (Client-side): The client checks if the nonce was indeed updated by the validateTransaction
call.
Fee Handling: The client proceeds to handle fee payments, which might involve calling payForTransaction
on the account or prepareForPaymaster
if a paymaster is used.
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.
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.
testZkValidateTransaction
FunctionWe 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:
Test Function Structure:
Arrange Phase:
Transaction details (dest
, value
, functionData
) are defined, similar to a standard transaction call setup.
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.
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:
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
.
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.
The bytes4
return value (the magic bytes) is captured in the magic
variable.
Assert Phase:
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.
_signTransaction
Helper for Test SignaturesTo 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.
Explanation of _signTransaction
:
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.
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.
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.
Copy Transaction: The input transaction
is copied to signedTransaction
to avoid modifying the original unsigned transaction struct if it's used elsewhere.
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.
setUp
Function Adjustments for Validation TestsThe 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:
These setup steps ensure that:
The signature generated by _signTransaction
(using ANVIL_DEFAULT_KEY
) will be valid because the minimalAccount
's owner is ANVIL_DEFAULT_ACCOUNT
.
The balance check within validateTransaction
will pass because the account has been funded with ETH using vm.deal
.
To run this specific test for validateTransaction
in a zkSync context, use the following Foundry command:
--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.
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)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on July 28, 2025
Duration: 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)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on July 28, 2025