Skip to content

How to use the ERC-20 Paymaster

When using Pimlico's ERC-20 Paymaster, the paymaster needs approval to spend funds on the payer's behalf. The amount to approve must be atleast equal to the userOperation's maxCost.

You can use the helper function below to get the required approval amount.

Steps

Define imports and create the clients

import { toSimpleSmartAccount } from "permissionless/accounts"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { createPublicClient, getAddress, type Hex, http, parseAbi } from "viem"
import {
	createBundlerClient,
	entryPoint07Address,
	UserOperation,
	type EntryPointVersion,
} from "viem/account-abstraction"
import { privateKeyToAccount } from "viem/accounts"
import { baseSepolia } from "viem/chains"
 
const pimlicoUrl = `https://api.pimlico.io/v2/${baseSepolia.id}/rpc?apikey=${process.env.PIMLICO_API_KEY}`
 
const publicClient = createPublicClient({
	chain: baseSepolia,
	transport: http("https://sepolia.base.org"),
})
const pimlicoClient = createPimlicoClient({
	chain: baseSepolia,
	transport: http(pimlicoUrl),
	entryPoint: {
		address: entryPoint07Address,
		version: "0.7" as EntryPointVersion,
	},
})
const bundlerClient = createBundlerClient({
	account: await toSimpleSmartAccount({
		client: publicClient,
		owner: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
	}),
	chain: baseSepolia,
	transport: http(pimlicoUrl),
	paymaster: pimlicoClient,
	userOperation: {
		estimateFeesPerGas: async () => {
			return (await pimlicoClient.getUserOperationGasPrice()).fast
		},
	},
})

Fetch your users' token holdings

For this example, we will assume that our user only holds USDC on Base Sepolia.

const usdc = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
const smartAccountAddress = bundlerClient.account.address
 
const senderUsdcBalance = await publicClient.readContract({
	abi: parseAbi(["function balanceOf(address account) returns (uint256)"]),
	address: usdc,
	functionName: "balanceOf",
	args: [smartAccountAddress],
})
 
if (senderUsdcBalance < 1_000_000n) {
	throw new Error("insufficient USDC balance, required at least 1 USDC.")
}

Call pimlico_getTokenQuotes and calculate the approval amount needed for your userOperation

After calling pimlico_getTokenQuotes, use the returned values to calculate the approval amount for a specific userOperation by calling using formula from the paymaster's getCostInToken function.

const quotes = await pimlicoClient.getTokenQuotes({
	tokens: [usdc],
})
const { postOpGas, exchangeRate, paymaster } = quotes[0]
 
const userOperation: UserOperation<"0.7"> = await bundlerClient.prepareUserOperation({
	calls: [
		{
			to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
			data: "0x1234" as Hex,
		},
	],
})
 
const userOperationMaxGas =
	userOperation.preVerificationGas +
	userOperation.callGasLimit +
	userOperation.verificationGasLimit +
	(userOperation.paymasterPostOpGasLimit || 0n) +
	(userOperation.paymasterVerificationGasLimit || 0n)
 
// using formula here https://github.com/pimlicolabs/singleton-paymaster/blob/main/src/base/BaseSingletonPaymaster.sol#L334-L341
const maxCostInToken =
	((userOperationMaxGas + postOpGas * userOperation.maxFeePerGas) * exchangeRate) / BigInt(1e18)
 

Reconstruct and send the userOperation

After finding the amount that we need to approve, we reconstruct the userOperation to include the token approval before sending it.

Ensure that the paymaster context includes a token field to inform the Pimlico endpoint to sponsor the userOperation using ERC-20 mode.

 
const hash = await bundlerClient.sendUserOperation({
	paymasterContext: {
		token: usdc,
	},
	calls: [
		{
			abi: parseAbi(["function approve(address,uint)"]),
			functionName: "approve",
			args: [paymaster, maxCostInToken],
			to: usdc,
		},
		{
			to: getAddress("0xd8da6bf26964af9d7eed9e03e53415d37aa96045"),
			data: "0x1234" as Hex,
		},
	],
})
 
const opReceipt = await bundlerClient.waitForUserOperationReceipt({
	hash,
})
 
console.log(`transactionHash: ${opReceipt.receipt.transactionHash}`)