5/5
## Constructing the zkSync Era Transaction Struct for Smart Account Interaction This lesson guides you through the process of building the `Transaction` struct, a fundamental component for interacting with zkSync Era smart contract accounts, particularly within a Foundry testing environment. We'll explore why this struct is necessary, how to implement a helper function to create it, and how to utilize it in your tests. ## The zkSync `Transaction` Struct: A Necessary Component When working with zkSync Era smart contract accounts, such as the `ZkMinimalAccount.sol` contract, a key difference from standard Ethereum account abstraction (like ERC-4337) emerges. While a typical Ethereum AA setup might involve a simple `execute` function call with parameters like `destination`, `value`, and `functionData`, zkSync's `executeTransaction` function demands a more comprehensive `Transaction memory _transaction` object as input. Think of this `Transaction` struct as zkSync's equivalent to the `PackedUserOperation` struct used in ERC-4337 on Ethereum. It serves to bundle all essential information for a transaction into a single, cohesive object. In previous Ethereum examples, a script (e.g., `SendPackedUserOp.s.sol`) might assist in creating the `UserOperation`. However, in our current zkSync test setup using Foundry, we'll need to craft a helper function directly within our test file (`ZkMinimalAccountTest.t.sol`) to construct this `Transaction` object. ## Implementing the `_createUnsignedTransaction` Helper Function The core of our task is to build a helper function that assembles an *unsigned* transaction struct. This unsigned transaction is suitable for testing scenarios where an action is initiated directly by the account owner. **1. Importing the `Transaction` Struct Definition** The `Transaction` struct is not a native Solidity type nor is it inherently part of standard Foundry. It must be imported from the `foundry-era-contracts` library. This library provides system contracts and helper utilities for zkSync Era development. Add the following import statement to your test file (e.g., `ZkMinimalAccountTest.t.sol`): ```solidity import {Transaction} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol"; ``` You can navigate to `MemoryTransactionHelper.sol` within the `foundry-era-contracts` library to inspect the detailed definition of the `Transaction` struct. **2. Defining the Helper Function** We'll place our helper function under a comment block for organization, typically something like `/*//////////// HELPERS ////////////*/`. The function signature will be: ```solidity function _createUnsignedTransaction( address from, uint8 transactionType, address to, uint256 value, bytes memory data ) internal view returns (Transaction memory) { // Implementation will go here } ``` **3. Understanding the `Transaction` Struct Fields** The `Transaction` struct, as defined in `MemoryTransactionHelper.sol`, contains numerous fields. Here's an overview of the key ones we'll be populating: * `txType`: (uint8) Specifies the type of transaction. For zkSync native Account Abstraction, we'll use type `113` (or `0x71`). * `from`: (uint256) The address initiating the transaction. Note: This is a `uint256`, not an `address` type. * `to`: (uint256) The target contract address. Also a `uint256`. * `gasLimit`: (uint256) The gas limit for the transaction. * `gasPerPubdataByteLimit`: (uint256) The maximum gas price per byte of pubdata. This is relevant for the cost of publishing data from L2 (zkSync) to L1 (Ethereum). * `maxFeePerGas`: (uint256) Similar to EIP-1559, the maximum fee per gas. * `maxPriorityFeePerGas`: (uint256) Similar to EIP-1559, the maximum priority fee per gas. * `paymaster`: (uint256) The address of the paymaster (0 if no paymaster is used). * `nonce`: (uint256) The transaction nonce for the `from` address. * `value`: (uint256) The amount of ETH (or native currency) being transferred with the transaction. * `data`: (bytes) The calldata for the transaction (e.g., function signature and arguments). * `reserved`: (uint256[4]) An array reserved for future protocol use. * `signature`: (bytes) The transaction signature. For our unsigned transaction, this will be empty. * `factoryDeps`: (bytes32[]) Hashes of contract bytecode needed for deployments initiated by this transaction. Empty if not deploying contracts. * `paymasterInput`: (bytes) Input data for the paymaster, if one is used. * `reservedDynamic`: (bytes) A dynamically-sized field reserved for future use. **4. Populating the Struct in the Helper Function** Now, let's fill in the implementation of `_createUnsignedTransaction`: ```solidity function _createUnsignedTransaction( address from, uint8 transactionType, address to, uint256 value, bytes memory data ) internal view returns (Transaction memory) { // Fetch the nonce for the 'minimalAccount' (our smart contract account) // Note: vm.getNonce is a Foundry cheatcode. In a real zkSync environment, // you'd query the NonceHolder system contract. uint256 nonce = vm.getNonce(address(minimalAccount)); // Initialize an empty array for factory dependencies bytes32[] memory factoryDeps = new bytes32[](0); return Transaction({ txType: transactionType, // e.g., 113 for zkSync AA from: uint256(uint160(from)), // Cast 'from' address to uint256 to: uint256(uint160(to)), // Cast 'to' address to uint256 gasLimit: 16777216, // Placeholder value (adjust as needed) gasPerPubdataByteLimit: 16777216, // Placeholder value maxFeePerGas: 16777216, // Placeholder value maxPriorityFeePerGas: 16777216, // Placeholder value paymaster: 0, // No paymaster for this example nonce: nonce, // Use the fetched nonce value: value, // Value to be transferred reserved: [uint256(0), uint256(0), uint256(0), uint256(0)], // Default empty data: data, // Transaction calldata signature: hex"", // Empty signature for an unsigned transaction factoryDeps: factoryDeps, // Empty factory dependencies paymasterInput: hex"", // No paymaster input reservedDynamic: hex"" // Empty reserved dynamic field }); } ``` Several placeholder values (e.g., for `gasLimit`, `gasPerPubdataByteLimit`) are used. In a real-world scenario, you would need to estimate these more accurately. For our testing purposes, these large values suffice as precise gas estimation is not the current focus. ## Key Considerations for zkSync Transactions When constructing zkSync `Transaction` structs, keep the following points in mind: * **Transaction Types (`txType`):** For zkSync native Account Abstraction, the `txType` is **113** (hex `0x71`). zkSync supports other transaction types (e.g., Legacy, EIP-2930, EIP-1559), but `113` is specific to AA. * **Address Casting (`uint256(uint160(address))`):** A critical detail is that the `from` and `to` fields in the zkSync `Transaction` struct are `uint256`, not the standard `address` type. The conversion `uint256(uint160(someAddress))` is the standard way to cast an `address` (which is 160 bits) to a `uint256`. This distinction arises from lower-level data representation, a topic often explored in depth in areas like EVM assembly or formal verification. * **Nonce Handling (`vm.getNonce`):** In our Foundry test, we use the `vm.getNonce(address)` cheatcode to retrieve the nonce for the `minimalAccount`. It's important to recognize that this is a simplification provided by Foundry. In a live zkSync Era environment, nonces are managed by a dedicated `NonceHolder` system contract. For robust, off-Foundry applications, you would interact with this `NonceHolder` contract to get the correct nonce. * **Pubdata (`gasPerPubdataByteLimit`):** This field accounts for the cost associated with publishing data from zkSync (L2) back to Ethereum (L1). This is a distinct cost factor in zkEVM rollups, reflecting the L1 data availability requirements. * **Unsigned Transaction:** Our helper function is named `_createUnsignedTransaction` because we explicitly set the `signature` field to an empty byte string (`hex""`). This is suitable for owner-initiated actions within tests where the signature verification path might be bypassed or handled differently. * **Factory Dependencies (`factoryDeps`):** This field is crucial if your transaction intends to deploy new contracts *through* the smart contract account. It holds the bytecode hashes of these new contracts. If your transaction only calls existing contracts, this array can be empty. * **Paymaster Fields (`paymaster`, `paymasterInput`):** These fields are used when implementing sponsored transactions via a paymaster. If you're not using a paymaster, `paymaster` should be the zero address, and `paymasterInput` can be empty. * **Reserved Fields:** These are populated with default empty/zero values as they are designated for future protocol enhancements or features. ## Utilizing the Helper Function in Your Tests With the `_createUnsignedTransaction` helper function in place, let's see how to use it within a test function, for example, `testZkOwnerCanExecuteCommands`. **1. Arrange Phase:** Your test will likely already have code to set up the destination address, value, and function data for the intended call. For instance, preparing a call to a USDC contract's `mint` function: ```solidity // Example setup (values may vary based on your specific test) address dest = address(usdc); // Target contract (USDC) uint256 value = 0; // No ETH transferred in this specific call bytes memory functionData = abi.encodeWithSignature("mint(address,uint256)", address(minimalAccount), AMOUNT); ``` **2. Create the Transaction Object:** Now, call your new helper function to construct the `Transaction` struct: ```solidity // Inside your test function, before the "Act" phase Transaction memory transaction = _createUnsignedTransaction( minimalAccount.owner(), // 'from' is the owner of the smart account 113, // Transaction type for zkSync AA (0x71) dest, // 'to' is the USDC contract address value, // 'value' is 0 for this mint call functionData // 'data' is the encoded mint function call ); ``` Here, `minimalAccount.owner()` provides the `from` address, signifying that the owner is initiating this action. **3. Act Phase:** To execute the transaction, you'll interact with your `minimalAccount` contract. * **Simulating the Caller (`vm.prank`):** Since we are testing an owner-initiated action, and the `executeTransaction` function on `ZkMinimalAccount.sol` likely checks that `msg.sender` is the owner (or the bootloader), we use Foundry's `vm.prank` cheatcode. This makes the subsequent call appear as if it originated from the `minimalAccount.owner()`. ```solidity vm.prank(minimalAccount.owner()); ``` * **Executing the Transaction:** Call the `executeTransaction` function on your `minimalAccount` instance, passing the constructed `transaction` object. ```solidity // Define EMPTY_BYTES32 if not already defined: // bytes32 constant EMPTY_BYTES32 = bytes32(0); minimalAccount.executeTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction); ``` The first two arguments to `executeTransaction` (`_txHash` and `_suggestedSignedHash`) are passed as `EMPTY_BYTES32`. These are typically provided by the bootloader in a full zkSync transaction flow. However, for an owner-initiated transaction where the `requireFromBootloaderOrOwner` modifier (or similar logic) is satisfied by the `msg.sender == owner` condition, these hashes may not be strictly validated for this specific execution path, allowing us to use placeholders. **4. Assert Phase:** Finally, assert the expected outcome of the transaction. For example, if the transaction was a USDC mint, verify that the `minimalAccount`'s USDC balance has increased: ```solidity assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT, "USDC balance should increase"); ``` ## Conclusion You have now learned how to identify the necessity for the zkSync `Transaction` struct, import its definition from the `foundry-era-contracts` library, and implement a crucial helper function, `_createUnsignedTransaction`. This function allows you to systematically populate the `Transaction` struct with the required details, including the correct transaction type, address casting, nonce management considerations, and placeholders for gas and unused fields. By integrating this helper with Foundry's `vm.prank`, you can effectively test owner-initiated actions on your zkSync Era smart contract accounts, calling `executeTransaction` and verifying the results, thereby enhancing your testing capabilities for zkSync dApps.
A developer's deep-dive to zkSync Transaction Structs for Smart Account Foundry Tests - Explore building the zkSync `Transaction` struct with a helper function in Foundry, vital for smart contract account testing. Master its fields, address casting, nonce handling, and execute owner-initiated transactions via `executeTransaction`.
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 June 10, 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 June 10, 2025