Building and Deploying a Gasless Paymaster
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 theCounter
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 theCounter
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, includingOwnable
, 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
, andforge-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:
- 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.
- 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.
- Extracts the selector (first 4 bytes of
- 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.
- 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.
- 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:
// 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 theGaslessPaymaster
contract.counter
– An instance of theCounter
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 fromforge-std
, providing scripting utilities.ScriptExt
– Extended scripting utilities fromforge-zksync-std
.- Through
ScriptExt
, we can access ZKsync-specific cheatcodes usingvmExt
(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’sOwnable
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:
- Deploy the
GaslessPaymaster
contract using CREATE2 - Fund the paymaster with 1 ETH
- 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
. 🚀