Building and Deploying a Gasless Paymaster

Learn how to build, and deploy, a Gasless Paymaster on ZKsync using Foundry-ZKsync.

Setting Up Your Project

In this guide, we will set up a Foundry-ZKsync project that integrates a Gasless Paymaster. By the end of this section, you will have a structured development environment ready for contract development, testing, and deployment on ZKsync.

Project Initialization

To get started, clone the template repository into a new project folder called zk-paymaster:

git clone git@github.com:dutterbutter/paymaster-foundry-guide.git zk-paymaster

This template includes the basic Foundry setup needed to begin development. Here’s what you’ll find inside:

  • src/ – Contains the Counter contract, which we will use in our paymaster demonstration.
  • script/ – Contains scripts for deploying contracts. We will update this to include our deployment script for the Gasless Paymaster.
  • test/ – Includes a Foundry-style test suite for the Counter contract.

What’s Missing?

The template does not yet include:

✅ The Gasless Paymaster contract
✅ A deployment script that utilizes ZKsync's paymaster features and cheatcodes

We’ll add these components next.

Installing Dependencies

To implement a Gasless Paymaster, we need a few key dependencies:

  • @zksync-contracts – Provides interfaces for interacting with ZKsync-specific smart contracts, including the paymaster system.
  • @openzeppelin-contracts – Contains reusable smart contract utilities, including Ownable, which we will use for access control.
  • forge-zksync-std – Extends Foundry with ZKsync-specific utilities, including cheatcodes for testing and debugging.

Installing Dependencies with Soldeer

For dependency management, we will use Soldeer, a dedicated Solidity package manager. If you prefer, you can install dependencies via Git submodules using forge install matter-labs/v2-testnet-contracts@beta.

However, for this guide, we will use Soldeer to install our required packages:

cd zk-paymaster
forge soldeer install @zksync-contracts~0.0.1
forge soldeer install @openzeppelin-contracts~5.2.0
forge soldeer install forge-zksync-std~0.0.1

What This Command Does:

  • Creates a dependencies/ folder to store the installed packages.
  • Installs @zksync-contracts, @openzeppelin-contracts, and forge-zksync-std.
  • Generates a remappings.txt file for clean import paths, simplifying contract development.

With our project set up and dependencies installed, we are now ready to write and deploy our Gasless Paymaster contract! 🚀

GaslessPaymaster Contract Code

Next, create a new file for our paymaster contract called GaslessPaymaster.sol.

touch src/GaslessPaymaster.sol

Copy and paste the full contract below into the file.

Understanding the GaslessPaymaster Contract

Let's break down the GaslessPaymaster contract and explain how it works.

Contract Overview

This GaslessPaymaster contract implements the IPaymaster interface from ZKsync and follows the General Paymaster Flow. Its primary role is to cover gas fees for transactions, making them gasless for users.

Unlike more complex paymaster implementations that apply additional validation logic, this contract accepts all transactions by default as long as they follow the required format.

Key Components

Constructor Function

constructor(address initialOwner) Ownable(initialOwner) {}
  • This initializes the contract and sets the initialOwner who has administrative privileges.
  • The Ownable contract from OpenZeppelin provides ownership management, allowing only the owner to withdraw funds from the paymaster.

Modifiers

onlyBootloader

modifier onlyBootloader() {
    require(
        msg.sender == BOOTLOADER_FORMAL_ADDRESS,
        "Only bootloader can call this method"
    );
    _;
}
  • Ensures that only the ZKsync bootloader (a system contract responsible for transaction execution) can call certain functions.

Functions

validateAndPayForPaymasterTransaction

function validateAndPayForPaymasterTransaction(
    bytes32,
    bytes32,
    Transaction calldata _transaction
) 
    external 
    payable 
    onlyBootloader 
    returns (bytes4 magic, bytes memory context)

This function is the core of the paymaster's logic. It validates transactions and pays gas fees if they follow the General Paymaster Flow.

How it works:

  1. Ensures Paymaster Input Format
    require(
        _transaction.paymasterInput.length >= 4,
        "The standard paymaster input must be at least 4 bytes long"
    );
    
    • Ensures the transaction’s paymaster input is valid by checking its length.
  2. Checks for General Paymaster Flow
    bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]);
    if (paymasterInputSelector == IPaymasterFlow.general.selector) {
    
    • Extracts the selector (first 4 bytes of paymasterInput) to verify it follows the General Paymaster Flow.
  3. Calculates the Required ETH for Gas Fees
    uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;
    
    • Computes the exact gas fee that the paymaster must pay to execute the transaction.
  4. Transfers the ETH to the Bootloader
    (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ value: requiredETH }("");
    require(success, "Failed to transfer tx fee to the Bootloader.");
    
    • The paymaster transfers the required gas fee directly to the ZKsync bootloader, covering the transaction’s cost and execute the transaction.
    • If the paymaster does not have enough funds, the transaction fails.
  5. Rejects Unsupported Paymaster Flows
    else {
        revert("Unsupported paymaster flow in paymasterParams.");
    }
    
    • If the transaction doesn’t follow the General Paymaster Flow, it is rejected.

Post-Transaction Processing

function postTransaction(
    bytes calldata _context,
    Transaction calldata _transaction,
    bytes32,
    bytes32,
    ExecutionResult _txResult,
    uint256 _maxRefundedGas
) external payable override onlyBootloader {}
  • This function is required by the IPaymaster interface but is left empty in this implementation.
  • It is usually used to perform post-transaction validation or refund unused gas in more advanced paymaster contracts.

Fund Management Functions

withdraw

function withdraw(address payable _to) external onlyOwner {
    uint256 balance = address(this).balance;
    (bool success, ) = _to.call{value: balance}("");
    require(success, "Failed to withdraw funds from paymaster.");
}
  • Allows the owner to withdraw all funds from the contract.
  • Ensures that only the contract owner (not users or other contracts) can manage the paymaster’s balance.

receive

receive() external payable {}
  • Allows the contract to receive ETH deposits, which are necessary to fund transactions.
  • This is critical because the paymaster needs ETH to cover users' gas fees.

Deploying Contracts

Create a new file in the script directory:

touch script/GaslessPaymaster.s.sol

Copy and paste the deploy script:

script/GaslessPaymaster.s.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {Script} from "forge-std/src/Script.sol";
import {ScriptExt} from "forge-zksync-std/src/ScriptExt.sol";
import {GaslessPaymaster} from "../src/GaslessPaymaster.sol";
import {Counter} from "../src/Counter.sol";

contract PaymasterScript is Script, ScriptExt {
  GaslessPaymaster public paymaster;
    Counter public counter;

    bytes private paymasterEncodedInput;

    function setUp() public {}

    function run() public {
        vm.startBroadcast();

        paymaster = new GaslessPaymaster{salt: "1234"}(msg.sender);
        (bool success,) = address(paymaster).call{value: 1 ether}("");
        require(success, "Failed to fund Paymaster.");

        paymasterEncodedInput = abi.encodeWithSelector(bytes4(keccak256("general(bytes)")), bytes(""));
        vmExt.zkUsePaymaster(address(paymaster), paymasterEncodedInput);
        counter = new Counter{salt: "1234"}();
        vm.roll(1);

        vm.stopBroadcast();
    }
}

Understanding the GaslessPaymaster.s.sol Deployment Script

This script deploys a Gasless Paymaster and a Counter contract, showcasing how to use the ZKsync paymaster mechanism with Foundry-ZKsync. It also demonstrates how to interact with ZKsync-specific cheatcodes through forge-zksync-std.

Key Components

Variables

The script defines two key contract instances:

  • paymaster – An instance of the GaslessPaymaster contract.
  • counter – An instance of the Counter contract.
  • paymasterEncodedInput – Stores the encoded input required for the General Paymaster Flow, which will be used for paymaster transactions.

Inheritance and Cheatcode Access

contract PaymasterScript is Script, ScriptExt
  • Script – The base class from forge-std, providing scripting utilities.
  • ScriptExt – Extended scripting utilities from forge-zksync-std.
  • Through ScriptExt, we can access ZKsync-specific cheatcodes using vmExt (e.g., vmExt.zkUsePaymaster).

The run Function

Step 1: Start Transaction Broadcasting

vm.startBroadcast();
  • startBroadcast() allows the script to execute transactions as the sender (msg.sender).
  • This ensures that contract deployments and transactions are properly recorded.

Step 2: Deploy and Fund the Paymaster using CREATE2

Using CREATE2 for Predictable Deployment Addresses

paymaster = new GaslessPaymaster{salt: "1234"}(msg.sender);
  • Deploys the GaslessPaymaster contract using CREATE2, which allows the contract address to be deterministically computed before deployment.
  • The salt parameter ("1234") ensures the deployment address remains consistent.
  • Passes msg.sender as the contract owner, ensuring that the deployer retains administrative control through OpenZeppelin’s Ownable contract.

Funding the Paymaster

(bool success,) = address(paymaster).call{value: 1 ether}("");
require(success, "Failed to fund Paymaster.");
  • Transfers 1 ETH to the paymaster contract, ensuring it has sufficient funds to cover gas fees for gasless transactions.
  • If the transfer fails, the script reverts, preventing deployment from proceeding with an unfunded paymaster.

Step 3: Encode the Paymaster Input

paymasterEncodedInput = abi.encodeWithSelector(
    bytes4(keccak256("general(bytes)")),
    bytes("")
);
  • Encodes the General Paymaster Flow selector, which is required when using a paymaster.

Step 4: Deploy the Counter Contract with the Paymaster Covering Fees

Using the zkUsePaymaster Cheatcode

Before deploying the Counter contract, we use the zkUsePaymaster cheatcode, which allows us to specify a paymaster to cover the gas fees for the next transaction.

vmExt.zkUsePaymaster(address(paymaster), paymasterEncodedInput);
  • vmExt.zkUsePaymaster(paymaster, paymasterEncodedInput);
    • This instructs the Foundry-ZKsync environment to use the specified paymaster contract for the next transaction.
    • The paymasterEncodedInput ensures that the transaction specifies the General Paymaster Flow, which allows the paymaster to cover the gas fees.
Deploy the Counter Contract using CREATE2
counter = new Counter{salt: "1234"}();
  • Deploys the Counter contract, which will later be used for gasless transactions.

Step 5: Stop Transaction Broadcasting

vm.stopBroadcast();
  • Ends the transaction broadcasting session.

Deploying to a Local Anvil-ZKsync Instance

anvil-zksync is included with Foundry-ZKsync, providing an easy way to run a local ZKsync node. This allows for fast and efficient contract deployment and testing without interacting with a live network.

Starting the Local Node

To start an anvil-zksync instance, run:

anvil-zksync

This will launch a local ZKsync environment and display a list of pre-funded accounts:

Rich Accounts
==================

(0) 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
...

Private Keys
==================

(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
...

These accounts can be used for deploying contracts and making transactions.

Running the Deployment Script

With the local node running, open a new terminal window and execute the deployment script:

forge script script/GaslessPaymaster.s.sol --zksync --slow --rpc-url anvil-zksync --broadcast --interactive 1

This script will:

  1. Deploy the GaslessPaymaster contract using CREATE2
  2. Fund the paymaster with 1 ETH
  3. Deploy the Counter contract using the paymaster to cover gas fees

After running the script, you will be prompted to enter a private key. Select one from the list displayed when launching anvil-zksync:

Enter private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Successful Script Execution Output

If the deployment is successful, you should see output similar to this:

Script ran successfully.

==========================

Chain 260

Estimated gas price: 0.090500001 gwei

Estimated total gas used for script: 10738910

Estimated amount required: 0.00097187136573891 ETH

==========================

##### 260
  [Success] Hash: 0x6051fd5de151f5f7226917062c0cd27ae8a0dfbb7b77990cd3af697bd88573a3
Contract Address: 0xd9498989Fada9e78798F696B17Ab6B3b5Fe65FDF
Block: 1
Paid: 0.00042912163275 ETH (9483351 gas * 0.04525 gwei)


##### 260
  [Success] Hash: 0xf0761c2c317767f970946a2622b17270125d555dddc72a859308426456377825
Block: 3
Paid: 0.00000981413675 ETH (216887 gas * 0.04525 gwei)


##### 260
  [Success] Hash: 0xdf38c0928f1c16d9104038724ce6742234ccae8bb35006a73b95392742ec2a3c
Contract Address: 0xb3EA1C4F4f0cF65767f0c870E63523c321e92003
Block: 5
Paid: 0.000081170717 ETH (1793828 gas * 0.04525 gwei)

 Sequence #1 on 260 | Total Paid: 0.0005201064865 ETH (11494066 gas * avg 0.04525 gwei
                                                         
==========================

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

This confirms that:

  • The GaslessPaymaster was deployed
  • The Counter contract was deployed using the paymaster to cover gas fees

Verifying Paymaster Usage

To confirm that the paymaster was used to pay for the Counter contract deployment, we can inspect the anvil-zksync logs:

 [SUCCESS] Hash: 0x6051fd5de151f5f7226917062c0cd27ae8a0dfbb7b77990cd3af697bd88573a3
Initiator: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Payer: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Gas Limit: 30_783_417 | Used: 9_483_351 | Refunded: 21_300_066
Paid: 0.0004291216 ETH (9483351 gas * 0.04525000 gwei)
Refunded: 0.0009638280 ETH


 [SUCCESS] Hash: 0xf0761c2c317767f970946a2622b17270125d555dddc72a859308426456377825
Initiator: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Payer: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Gas Limit: 508_396 | Used: 216_887 | Refunded: 291_509
Paid: 0.0000098141 ETH (216887 gas * 0.04525000 gwei)
Refunded: 0.0000131908 ETH


 [SUCCESS] Hash: 0xf7c8908f3fef0def0ad2e2b5da276444a4d4477fb2f60a923b35be5b238b1227
Initiator: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Payer: 0xd9498989fada9e78798f696b17ab6b3b5fe65fdf
Gas Limit: 5_535_734 | Used: 1_776_909 | Refunded: 3_758_825
Paid: 0.0000804051 ETH (1776909 gas * 0.04525000 gwei)
Refunded: 0.0001700868 ETH

Key observations:

  • The first two transactions were paid for by the user’s account (0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266).
  • The third transaction (Counter contract deployment) was paid by the GaslessPaymaster (0xd9498989Fada9e78798F696B17Ab6B3b5Fe65FDF).

This confirms that the zkUsePaymaster cheatcode successfully instructed the paymaster to cover gas fees for the contract deployment.

Checking Paymaster Balance After Deployment

Since the paymaster was initially funded with 1 ETH, we expect the balance to have decreased due to covering the deployment costs. We can check the balance using cast:

cast balance 0xd9498989Fada9e78798F696B17Ab6B3b5Fe65FDF --rpc-url anvil-zksync | cast from-wei

Output:

0.999919594867750000

This confirms that the paymaster's balance has decreased as expected, proving that it successfully covered the gas fees.


Next Steps: Interacting with the Paymaster

Now that we have deployed and validated the GaslessPaymaster, the next step is to demonstrate how users can interact with it to execute transactions without paying gas fees using cast. 🚀


Made with ❤️ by the ZKsync Community