1/5
## Implementing Fee Payment in zkSync Native Account Abstraction Welcome to this lesson where we'll delve into a crucial aspect of zkSync native account abstraction: implementing the `payForTransaction` function. With this addition, our smart contract will be very close to a complete, functional native account on zkSync, capable of validating, paying for, and executing transactions. ### Understanding Payment Flow: zkSync Native AA vs. Ethereum ERC-4337 A key difference between zkSync's native account abstraction and Ethereum's ERC-4337 standard lies in how transaction payments are handled. In the Ethereum ERC-4337 model, pre-funding and payment typically occur within the `validateUserOperation` function. This function is responsible for checking if the account can pay and often for deducting the fees. However, in zkSync's native account abstraction, the process is slightly different. The `validateTransaction` function, which we've explored previously, primarily focuses on verifying if the account *can* pay – for instance, by checking if it possesses a sufficient balance of the required fee token. The actual execution of the payment is delegated to a separate function, which is the focus of today's lesson. ### Transaction Lifecycle and the Timing of Payment In the zkSync native AA model, the actual payment for a transaction occurs during the validation phase, specifically *before* the transaction is submitted to the main node or sequencer for execution. The zkSync API client plays a vital role here. After successfully validating the transaction's nonce and other preliminary checks, the client calls either `payForTransaction` (if the account pays for itself) or `prepareForPaymaster` (if a paymaster is involved). This happens *before* the `executeTransaction` function is invoked by the main node. Let's look at the typical lifecycle phases: * **Phase 1: Validation** * Initial checks (e.g., nonce, signatures). * The `validateTransaction` function is called on the account contract. * The zkSync API client calls `payForTransaction` (or `prepareForPaymaster` followed by `validateAndPayForPaymasterTransaction` if a paymaster is used). * The zkSync API client verifies that the bootloader (the system contract responsible for initiating transactions) has been paid. * **Phase 2: Execution** * The zkSync API client passes the validated and pre-paid transaction to the main node/sequencer. * The main node calls `executeTransaction` on the account contract. This sequence ensures that fees are secured before resources are committed to full transaction execution. ### The Roles of `payForTransaction` and `prepareForPaymaster` Within your account contract, two primary functions are designated for handling fee payments: 1. **`payForTransaction`**: This function is invoked when the account itself is directly paying the transaction fees. 2. **`prepareForPaymaster`**: This function is called if a paymaster is sponsoring the transaction. It handles the initial interaction and setup required for the paymaster flow. In our current example, we are focusing on a minimal account that pays its own fees, so `payForTransaction` will contain the core logic, while `prepareForPaymaster` will be simpler. ### Code Implementation Details Let's examine the Solidity code for these payment functions. #### `prepareForPaymaster` Function Since our example does not utilize a paymaster, the `prepareForPaymaster` function will be intentionally left empty. It's declared to satisfy the interface expected by the zkSync system, but its body will contain no logic. ```solidity function prepareForPaymaster( bytes32 _txHash, // The hash of the transaction bytes32 _possibleSignedHash, // The hash that could be signed by the user Transaction memory _transaction // The transaction itself ) external payable { // This function is intentionally left empty. // In this example, we are not using a paymaster. // If a paymaster were to be used, this function would handle // the necessary preparations, such as approving token transfers // to the paymaster or other paymaster-specific logic. } ``` The `payable` modifier is present because, in paymaster scenarios, this function might need to handle value transfers, although it doesn't in our current context. #### `payForTransaction` Function This is where the actual fee payment logic for self-paying accounts resides. ```solidity function payForTransaction( bytes32 /*_txHash*/, // The hash of the transaction bytes32 /*_suggestedSignedHash*/, // The hash that was signed by the user Transaction memory _transaction // The transaction itself ) external payable { // In this minimal implementation, we can ignore _txHash and _suggestedSignedHash. // All necessary information for payment is contained within the _transaction struct. // The core logic relies on a helper function, payToTheBootloader, // which is part of the TransactionHelper library (via _transaction). bool success = _transaction.payToTheBootloader(); // If the payment to the bootloader fails, revert the transaction. if (!success) { revert ZkMinimalAccount__FailedToPay(); } } ``` **Key aspects of `payForTransaction`:** * **Parameters:** It receives `_txHash`, `_suggestedSignedHash`, and the `Transaction memory _transaction` struct. For our minimal implementation, we primarily use the `_transaction` struct, as it contains all necessary details like `maxFeePerGas` and `gasLimit`. * **`payable` Modifier:** This function must be `payable` because it will be transferring native ETH (or the chain's native currency) to the bootloader to cover transaction fees. * **`_transaction.payToTheBootloader()`:** The actual payment mechanism is abstracted into a helper function called `payToTheBootloader`. This function is typically part of a library, such as `TransactionHelper.sol` or, in our case, `MemoryTransactionHelper.sol`, and is made available as a method on the `_transaction` struct. * **Error Handling:** If `payToTheBootloader()` returns `false` (indicating payment failure), the function reverts with a custom error, `ZkMinimalAccount__FailedToPay()`. #### The `payToTheBootloader` Helper Function (Conceptual) The `payToTheBootloader` function, likely found in a utility library like `MemoryTransactionHelper.sol`, handles the low-level details of the payment. Conceptually, it performs the following: 1. **Gets the Bootloader Address:** It retrieves the `BOOTLOADER_FORMAL_ADDRESS`, a system-defined address to which fees are paid. 2. **Calculates Payment Amount:** It calculates the total fee amount, typically as `_transaction.maxFeePerGas * _transaction.gasLimit`. 3. **Executes Payment:** It makes a low-level call (e.g., using assembly for gas efficiency and direct control) to transfer the calculated `amount` to the `bootloaderAddr`. A simplified representation (actual implementation might use assembly for `call`): ```solidity // Snippet from a helper library like MemoryTransactionHelper.sol import {BOOTLOADER_FORMAL_ADDRESS} from "zksync/Constants.sol"; library TransactionHelper { struct Transaction { // ... other fields like nonce, to, data, gasLimit, maxFeePerGas ... } function payToTheBootloader(Transaction memory _transaction) internal returns (bool success) { address bootloaderAddr = BOOTLOADER_FORMAL_ADDRESS; uint256 amountToPay = uint256(_transaction.gasLimit) * _transaction.maxFeePerGas; // Ensure the contract has enough balance if it's paying with its own ETH if (address(this).balance < amountToPay) { return false; } // Perform the transfer to the bootloader // In a real scenario, this uses a low-level call. // For example: // (success, ) = bootloaderAddr.call{value: amountToPay}(""); // For demonstration: payable(bootloaderAddr).transfer(amountToPay); success = true; // Assume success for this simplified example if transfer doesn't revert return success; } } ``` *Note: The actual `payToTheBootloader` implementation in zkSync's system contracts or recommended libraries would use optimized assembly for the call to avoid overhead and ensure correct interaction with the bootloader.* #### New Error Definition To provide clear feedback on payment failures, we define a custom error: ```solidity error ZkMinimalAccount__FailedToPay(); ``` This error is used in the `payForTransaction` function when the call to `_transaction.payToTheBootloader()` does not succeed. ### Summary and Next Steps By implementing `payForTransaction`, our zkSync native account abstraction smart contract now has the capability to handle its own transaction fees. This is a critical step, as transactions will not be processed on zkSync unless the payment mechanism is correctly implemented, either through direct payment or via a paymaster. Remember: * Payment in zkSync native AA happens *before* `executeTransaction`. * The `payForTransaction` function is responsible for transferring fees to the bootloader. * If using paymasters, the `prepareForPaymaster` and subsequent paymaster-specific functions would handle the fee logic. With validation, execution, and now payment logic in place, the core functionalities of our native account abstraction wallet are largely complete. We are now much closer to deploying and interacting with a fully operational account on the zkSync network.
A vital lesson to Implementing Fee Payment in zkSync Native Account Abstraction - Implement the `payForTransaction` function, empowering your zkSync native account to manage its own transaction fees. You'll also explore `prepareForPaymaster`, zkSync's distinct pre-execution payment mechanism, and the `payToTheBootloader` helper.
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