Skip to main content
TLDR:
  • Shows how to deploy a custom paymaster on zkSync Era, sponsoring user transactions in ERC-20 tokens.
  • Uses Hardhat, TypeScript, and zkSync’s account abstraction to bypass ETH-only gas fees.
  • Demonstrates token minting to a zero-ETH wallet, with the paymaster contract covering transaction costs.
  • Provides a complete workflow for contract setup, funding, and gasless (sponsored) interaction on zkSync Era.

Introduction

This tutorial provides a step-by-step guide on creating and deploying a custom paymaster smart contract on the zkSync Era Testnet using Hardhat and TypeScript. By leveraging zkSync Era’s account abstraction feature, you’ll be able to facilitate transactions where gas fees are paid using a custom ERC-20 token, rather than ETH. The tutorial will guide you through deploying two smart contracts: a paymaster contract and an ERC-20 token contract. The paymaster contract, once deployed and funded with zkSync Era Testnet ETH, will act as a sponsor for users’ gas fees. In this setup, users can execute transactions without directly paying for gas. Instead, they will transfer a token to the paymaster contract, which handles the gas fee payment. This approach can, for example, offer a monthly quota of sponsored transactions for your DApp users. This enhances the user experience and makes DApps more accessible.

What is zkSync Era?

zkSync is a trustless zkEVM protocol based on ZK rollups that offers scalable and low-cost transactions on Ethereum by conducting computation and storing most data off-chain. As a subset of zkSync, zkSync Era is designed to emulate Ethereum’s functionality but at a lower cost. zkSync Era supports existing Ethereum wallets without separate registration and permits smart contract usage, like its Ethereum counterpart. Transactions in zkSync Era undergo a lifecycle that includes user initiation, operator processing, block commitment, and final block verification, all of which ensure the same level of security as Ethereum’s mainchain. The current version of zkSync Era caters to the needs of most applications on Ethereum, with features such as native support of ECDSA signatures, Solidity 0.8.x support, compatibility with Ethereum’s Web3 API, support for Ethereum cryptographic primitives, and L1 to L2 smart contract messaging. It also offers high security and usability compared to other L2 scaling solutions, with unique properties like safely withdrawing assets even when the user or ZK rollup validators are offline.
Learn more about zkEVM and rollups in zkEVM and rollups explained.

What is account abstraction?

zkSync Era natively supports account abstraction, a feature designed to address inefficiencies in Ethereum’s account system, which currently divides into externally owned accounts (EOAs) and smart contract accounts. These two types have limitations that can lead to friction in certain applications like smart-contract wallets or privacy protocols. However, account abstraction allows accounts on zkSync Era, an EVM-compatible chain, to initiate transactions akin to EOAs and implement arbitrary logic akin to smart contracts. zkSync Era introduces the concept of smart accounts and paymasters, designed to enhance flexibility, user experience, and security by offering fully programmable accounts and transaction sponsorship in ERC-20 tokens, respectively. Despite some similarities with Ethereum’s EIP 4337, key differences shape zkSync’s unique account management approach.

Prerequisites

Overview

To get from zero to a paymaster-sponsored transaction, do the following:
  1. With Chainstack, create a .
  2. With Chainstack, join the zkSync Era Sepolia Testnet.
  3. With Chainstack, access your zkSync Era Sepolia Testnet node credentials.
  4. Setup the project and install the required dependencies.
  5. Create the smart contracts.
  6. Get zkSync Era Sepolia Testnet funds from the faucet.
  7. Set up the Hardhat scripts to deploy and interact.
  8. Deploy the smart contracts.
  9. Use the paymaster to mint tokens to a new address without using gas.

Step-by-step

Create a public chain project

See Create a project.

Join the zkSync Era Sepolia Testnet

See Join a public network.

Get your zkSync Era Sepolia Testnet node endpoint

See View node access and credentials.

Setup the project and install the required dependencies

Create a new directory where you want to develop your project:
Initialize a new yarn project:
To create the project structure, we’ll need a contracts and a deploy directories:
Install the required dependencies:
Create a .env file and add your Chainstack zkSync Era endpoint and the private key for the account you want to use to deploy the smart contracts and get some gas tokens initially minted to.
Create a new hardhat.config.tsfile and paste the following:
Check the zksolc compiler GitHub repository to find the latest version available.

Create the smart contracts

Now that the project is set up let’s create the smart contracts starting from the ERC-20 token that users will use to pay ‘pay’ for transactions.

ERC-20 smart contract

In the contracts directory, create a new file named CSgas.sol and paste the following Solidity code:
This is a simple implementation of an ERC-20 smart contract using the OpenZeppelin library, which provides secure and community-audited implementations for parts of the Ethereum protocol. The smart contract is written in version 0.8.0 of Solidity, specified at the top of the file. The caret symbol ^ means it’s compatible with any new minor releases under version 0.8. The contract is importing something important—the ERC-20 token standard from the OpenZeppelin library. The contract itself is named CSgas. When CSgas is deployed, it calls a constructor function. This function sets up the token’s name, symbol, and the number of decimal places the token will have. The deploying code will pass through these details. The _decimals variable is used to determine how the token’s value is displayed in wallets and user interfaces. Most tokens go with 18 decimal places, matching the precision of Ethereum’s own currency, ether. The mint function is public, meaning anyone can call it. When called, it mints new tokens. You need to input who should receive the tokens and how many to create. It uses the _mint function from the ERC20 contract to create the token and update the balance.

Paymaster smart contract

Now let’s go over the interesting part, the Paymaster smart contract. Create a new file in the contract directory named Paymaster.sol and paste the following:
Let’s go over it. This contract is named Paymaster and it’s designed to manage transaction fees on behalf of users and uses Solidity version 0.8.0. The contract starts by importing dependencies. It uses the OpenZeppelin library for the IERC20 interface, which is a standard interface for interacting with ERC-20 tokens. It’s also importing several interfaces and libraries from the @matterlabs/zksync-contracts package, which is used for handling zkSync transactions. The Paymaster contract itself implements the IPaymaster interface. This interface defines the methods that a paymaster contract should have. The contract has a constant PRICE_FOR_PAYING_FEES set to 1 ether. This is the amount of tokens the user needs to allow the paymaster to spend on their behalf to pay for transaction fees. The allowedToken variable stores the address of the ERC-20 token that the paymaster will accept for transaction fees. This is passed through the deploying script after the ERC-20 token is deployed. The onlyBootloader modifier is a security feature. It ensures that only the bootloader can call certain functions. In the constructor, the contract sets the allowedToken to the address of the ERC-20 token that’s passed in when the contract is deployed. The validateAndPayForPaymasterTransaction function is where the magic happens. It’s called by the bootloader to validate and pay for a transaction. This function checks that the transaction is using the correct token and that the user has allowed the paymaster to spend enough tokens to cover the transaction fees. If everything checks out, the paymaster pays the transaction fees. The postTransaction function is called after a transaction is executed. It doesn’t do anything right now because refunds are not supported yet. Finally, thereceive function is a special function called when someone sends ether to the contract without providing any data. This function is empty, so the contract accepts ether without doing anything else.

Get zkSync Era Sepolia Testnet funds from the faucet

Before moving on, let’s make sure we have testnet funds to deploy smart contracts and interact with them on the zkSync Testnet. You can request funds from the Chainstack zkSync faucet.
Make sure to select zkSync Era Testnet on the bottom right of the screen.

Set up the Hardhat scripts to deploy and interact

At this stage, we can make the scripts to interact with zkSync Era. We’ll make two scripts:
  • A deployment script that will deploy and fund the smart contracts.
  • An interaction script that will use the paymaster to allow a user with no ETH mint tokens to a new wallet.

Script to deploy the smart contracts

Create a script to deploy those smart contracts in the deploy directory create a new file named deploy.ts and paste the following code:
Note that the directory must be named deploy, otherwise the zkSync version of Hardhat will not find the files.
This script has a lot going on, so let’s break it down:
  1. Imports and anvironment variables. The script begins by importing necessary modules and types. It then defines several constants and retrieves environment variables. Here is how we set up the script and keep it somewhat easier to maintain and edit:
  2. Environment variable checks. The script checks if the necessary environment variables are set. If they are not, it throws an error. This is to accommodate the fact that TypeScript does not allow for strings to have undefined values.
  3. Function definitions. The script defines several helper functions to keep the code clean and maintainable:
    • createWallet() — creates a new random wallet and logs the wallet’s address and private key. We create a new wallet to demonstrate how an account can initiate transactions sponsored by the paymaster without having any ETH.
    • deployContract() — deploys a contract to the zkSync Era network. It takes a Deployer instance, the name of the contract, and an array of arguments for the contract’s constructor. It loads the contract’s artifact, deploys the contract, waits for a certain number of confirmations, and then logs the contract’s address.
    • verifyContract() — verifies a contract on the zkSync Era network. It takes a HardhatRuntimeEnvironment instance, the contract’s address, the path to the contract’s source code, and an array of arguments passed to the contract’s constructor. It runs the verify:verify task provided by the Hardhat environment.
    • fundPaymaster() — funds the paymaster contract with a certain amount of ETH. It takes a Deployer instance and the paymaster contract’s address. It sends a transaction to the paymaster contract and waits for a certain number of confirmations.
    • mintTokens() — mints a certain number of tokens and sends them to a specified address. It takes a contract instance and an address. It calls the mint function on the contract and waits for the transaction to be confirmed.
  4. Main function. The script exports a default async function that performs the main deployment process:
    • It initializes a Provider instance, a Wallet instance, and a Deployer instance.
    • It creates a new wallet.
    • It deploys the CSgas contract, the token sent to the paymaster to indicate the account is allowed to get the transaction sponsored, and the Paymaster contract.
    • It verifies both contracts.
    • It funds the paymaster contract with ETH and mints tokens to the new wallet.
    • It retrieves and logs the balance of the paymaster contract and the new wallet’s token balance.
  5. Error handling. If any errors occur during deployment, the script catches them and logs the error message.

Script to interact

In the same deploy directory, create a new file named gasless-tx.ts and paste the following code:
Let’s break down the code and the logic:
  1. Imports and environment variables. The script begins by importing necessary modules and types. It then defines several constants and retrieves environment variables. Let’s go over what’s happening with the constants as is our setup point.
  2. Function definitions. The script defines several helper functions:
    • getToken() — retrieves the instance of the Chainstack gas token. It takes a HardhatRuntimeEnvironment instance and a Wallet instance reads the token’s artifact and returns a new contract instance with the token’s address and ABI. This is used to interact with the token contract.
    • createWallet() — creates a new random wallet and logs the wallet’s address and private key. We create another random wallet to use as a second user, where basically the first user we created in the deployment script mints new tokens for this second new user.
    • displayBalances() — retrieves and logs the balances of the user and the paymaster contract in both ETH and Chainstack gas tokens. It takes a Provider instance, a contract instance, a Wallet instance, and the paymaster contract’s address. This is just used to give insights into the process and logic.
    • mintTokens() — mints a certain number of tokens and sends them to a specified user. It takes a contract instance, a Wallet instance, and the paymaster parameters. It calls the mint function on the contract and waits for the transaction to be confirmed.
  3. Main function. The script exports a default async function that performs the main process:
    • It initializes a Provider instance and a Wallet instance, and retrieves the Chainstack gas token instance.
    • It displays the initial balances of the user and the paymaster contract.
    • It creates a new wallet.
    • It retrieves the current gas price and sets the paymaster parameters for an ApprovalBased flow.
    • It estimates the gas fee for minting new tokens and logs the estimated fee.
    • It mints tokens to the new wallet.
    • It displays the user’s final balances, the paymaster contract, and the new wallet’s token balance.
  4. Error handling. If errors occur during the process, the script catches them and logs the error message.

Deploy the smart contracts

At this point, everything is set up to deploy the contracts. Make sure to edit the configuration constants as you need:
Then in the terminal from the root of your project, compile the smart contracts:
This will compile the smart contracts in the contracts directory. Then run the deploy.ts script:
This process will generate a new wallet and deploy the contracts while transferring 0.001 ETH to the paymaster contract. It will also mint 10 CSgas tokens to the new wallet. Please note that this ETH is drawn from the account associated with the private key you specified in the .env file. The console will log the following result:
You can now verify the contracts on the zkSync explorer. You will see the deployment transactions and the verified contracts. You can check the contracts we deployed: Now the smart contracts are deployed. The paymaster should have 0.001 ETH to sponsor gas fees, and the new account should have 10 CSG tokens.

Use the paymaster to mint tokens to a new address without using gas

Now we can use the second script to interact with the paymaster contract and see the magic happening. First, make sure to update the configuration constants in the gasless-tx.ts file with the smart contract addresses, token name, and generated walled private key. In my specific case, it will look like this; note that you will have different parameters:
You can find those parameters in the deployment log; note that the token name depends on which name you gave:
Run the script with the following command:
The console will output the following logs:
Now, let’s dive into the sequence of events and the underlying logic: Initially, the script assesses the balances of all accounts. This is done to illustrate that the first user (created during the deployment phase) has a zero balance, rendering it incapable of sending a transaction due to the lack of gas. Despite this, the account holds 10 Chainstack gas tokens. On the other hand, the paymaster account holds a balance of 0.01 ETH but lacks any Chainstack gas tokens.
Next, the script generates a new wallet, which will serve as the recipient of the freshly minted tokens. The first account initiates the minting process, which, interestingly, does not pay for the gas in ETH. Instead, it transfers 1 CSG token to the paymaster contract. The paymaster contract, recognizing the token as an acceptable form of payment, sponsors the gas for the minting transaction. It’s important to note that if the script is configured to send less than 1 CSG token to the paymaster, or a different token, the transaction will fail and be reverted. This is a really powerful concept as it allows your DApp to onboard users more easily. In this specific example, we don’t do anything particularly meaningful, but you can build on top of this.

Conclusion

This tutorial guided you through setting up Hardhat to work with zkSync Era and develop a paymaster contract to sponsor users’ gas fees.

About the author

Davide Zambiasi

Davide Zambiasi Developer Advocate @ Chainstack
BUIDLs on EVM, The Graph protocol, and Starknet
Helping people understand Web3 and blockchain development
Last modified on April 8, 2026