Create Token Sponsor
Allow users to pay gas in ERC-20 tokens.
FunWallets allow users to pay for gas fees using any ERC20 token. Under the hood you must deposit a certain amount of the native gas token (ETH, MATIC, etc.) into the Token Paymaster contract. Next, the token paymaster will accept the desired ERC20 tokens in exchange for paying for gas. This feature gives additional utility for your token and allows you to sponsor gas fees for your users.
You can either use a TokenSponsor, or build your own.
This guide will show you how to configure a gasless paymaster sponsor.
Create Fun Wallet
Before we configure a paymaster, we create a FunWallet so we have a wallet to test the TokenSponsor with.
erc20-gas.js
const { FunWallet } = require("fun-wallet")const { Eoa } = require("fun-wallet/auth")const { fundWallet } = require("fun-wallet/utils")const API_KEY = "MYny3w7xJh6PRlRgkJ9604sHouY2MTke6lCPpSHq"const PRIVATE_KEY = "0x98e9cfb323863bc4bfc094482703f3d4ac0cd407e3af2351c00dde1a6732756a"const auth = new Eoa({ privateKey: PRIVATE_KEY })const uniqueId = auth.getUniqueId()// NOTE: Wallet must be prefunded if not using a sponsor// await fundWallet(funder_auth, wallet, .005)const funWallet = new FunWallet( { uniqueId } )
This code will fail. Continue below.
Give sponsor approval to spend from your wallet
Approve fund removal from the sponsor address.
// Approve paymaster address to access predefined set of wallet fundsawait funWallet.approve(auth, {spender: "0x07Ac5A221e5b3263ad0E04aBa6076B795A91aef9",token: "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",amount: 10000})
funWallet.approve(...) will require a signature.
When giving the paymaster contract approval over spending funds from the wallet for gas, the approved amount is measured in gas, not in transaction volume. For example, assume you approve the paymaster to spend 1,000 USDC. And the user transfers 500 USDC which costs 1 USDC worth of gas. The FunWallet will then have 999 USDC worth of spending approval.
If you had a high enough amount in the funWallet.approve(...) call, it should only be required to run once.
Add ERC-20 Token to the token paymaster
Configure the token paymaster to accept usdc as the token to pay with. Note that you can swap out "usdc" with any ERC-20 token address.
// Configure paymaster token as the ERC20 to pay withconst paymasterToken = "usdc"await configureEnvironment({gasSponsor: {sponsorAddress: funderAddress,token: paymasterToken,}})
Creating the paymaster
Deploy the paymaster contract and add the ERC-20 token to the whitelist to be accepted as payment for gas.
// Create the token paymasterconst gasSponsor = new TokenSponsor()const addTokens = await gasSponsor.addWhitelistTokens([paymasterToken])await funder.sendTx(addTokens)
Approve Tokens
Approve the ERC-20 token to the paymaster. This allows the paymaster to transfer the gas token from your wallet to the paymaster in the next step.
// Add gas to the paymasterconst baseStakeAmount = 1 // 1 ETH or native gas tokenconst paymasterTokenStakeAmount = 1_000_000 // 1 USDC (10**6 decimals)const approve = await gasSponsor.approve(paymasterToken, paymasterTokenStakeAmount * 2)await funder.sendTx(approve)
Stake ERC-20 Tokens
Stake the ERC-20 token to walletAddress. This allows walletAddress to pay for gas using the gas token you just staked for them.
const deposit = await gasSponsor.stakeToken(paymasterToken, walletAddress, paymasterTokenStakeAmount)await funder.sendTx(deposit)
Stake Native Gas Token
Stake the gas token to funderAddress. This allows funderAddress to pay for gas using the gas token you just staked for them.
const data = await gasSponsor.stake(funderAddress, baseStakeAmount)await funder.sendTx(data)
(Optional) Whitelisting or Blacklisting Users
See how to set permissions on the paymaster through whitelisting and blacklisting.
Full Code
An end to end example for creating a token paymaster and sending transactions without gas.
Note, this script is for the Goerli testnet, so you will need to change the chain id to a network fun-wallet supports if you want a different network.
Note, this script will run actual transactions on the network, so you will need to fund the funder wallet with some ETH or use our PRIVATE_KEY for the funder wallet.
Finally, this script will take a few minutes to run as it submits real transactions to the network so please be patient!
custom-paymaster.js
const { FunWallet, configureEnvironment } = require("fun-wallet")const { Eoa } = require("fun-wallet/auth")const { fundWallet } = require("fun-wallet/utils")const { TokenSponsor } = require("fun-wallet/sponsors/TokenSponsor")const PRIVATE_KEY = "0x8996148bbbf98e0adf5ce681114fd32288df7dcb97829348cb2a99a600a92c38"const API_KEY = "MYny3w7xJh6PRlRgkJ9604sHouY2MTke6lCPpSHq"const paymasterToken = "usdc"async function TokenPaymasterExample() {let auth = new Eoa({ privateKey: PRIVATE_KEY })let funder = new Eoa({ privateKey: PRIVATE_KEY })const options = {apiKey: API_KEY,}await configureEnvironment(options)console.log("Creating FunWallets...")const wallet = new FunWallet({ uniqueId: await auth.getUniqueId(), index: Math.ceil(Math.random() * 100_000_000) })const wallet1 = new FunWallet({ uniqueId: await auth.getUniqueId(), index: Math.ceil(Math.random() * 100_000_000) })const walletAddress = await wallet.getAddress()const walletAddress1 = await wallet1.getAddress()const funderAddress = await funder.getUniqueId()await fundWallet(funder, wallet, 1)await fundWallet(auth, wallet1, 1)console.log("Auth Wallet Created at: ", await auth.getUniqueId())console.log("Funder Wallet Created at: ", funderAddress)await wallet.swap(auth, {in: "eth",amount: 0.01,out: paymasterToken,options: {returnAddress: funderAddress}})await configureEnvironment({gasSponsor: {sponsorAddress: funderAddress,token: paymasterToken,}})const gasSponsor = new TokenSponsor()const addTokens = await gasSponsor.addWhitelistTokens([paymasterToken])console.log("Gas Sponsor Created at: ", await gasSponsor.getPaymasterAddress())console.log("Gas Token Address: ", await gasSponsor.getTokenInfo(paymasterToken))const baseStakeAmount = 1const paymasterTokenStakeAmount = 100const approve = await gasSponsor.approve(paymasterToken, paymasterTokenStakeAmount * 2)const deposit = await gasSponsor.stakeToken(paymasterToken, walletAddress, paymasterTokenStakeAmount)const deposit1 = await gasSponsor.stakeToken(paymasterToken, walletAddress1, paymasterTokenStakeAmount)const data = await gasSponsor.stake(funderAddress, baseStakeAmount)console.log("Adding Gas to the Paymaster...")await funder.sendTxs([approve, deposit, deposit1, data, addTokens])const setWhitelistModeTx = await gasSponsor.setToWhitelistMode()const addSpenderToWhitelistTx = await gasSponsor.addSpenderToWhiteList(await wallet.getAddress())const removeSpenderFromWhitelistTx = await gasSponsor.removeSpenderFromWhiteList(await wallet.getAddress())const whitelistTx = await auth.sendTxs([setWhitelistModeTx, addSpenderToWhitelistTx, removeSpenderFromWhitelistTx])console.log("Whitelist Transaction Hashes: ", whitelistTx[0].transactionHash, whitelistTx[1].transactionHash, whitelistTx[2].transactionHash)const setBlacklistModeTx = await gasSponsor.setToBlacklistMode()const addSpenderToBlacklistTx = await gasSponsor.addSpenderToBlackList(await wallet.getAddress())const removeSpenderFromBlacklistTx = await gasSponsor.removeSpenderFromBlackList(await wallet.getAddress())const blacklistTx = await auth.sendTxs([setBlacklistModeTx, addSpenderToBlacklistTx, removeSpenderFromBlacklistTx])console.log("Blacklist Transaction Hashes: ", blacklistTx[0].transactionHash, blacklistTx[1].transactionHash, blacklistTx[2].transactionHash)console.log("Transferring some funds... without gas!")await wallet.transfer(auth, {amount: .0001,to: "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE",})console.log("Sent ", .0001, "ETH to 0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE from ", await wallet.getAddress())}TokenPaymasterExample()