Skip to content

ERC-20 Paymaster Guides

Estimating Approval Amounts

When using Pimlico's ERC20-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 prefund value.

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

// Calculates amount of tokens that need to be approved for ERC20 paymaster to sponsor userOperation
const getApprovalAmount = async ({
	publicClient,
	paymasterAddress,
	userOperation,
}: EstimateParams) => {
	const erc20Paymaster = getContract({
		client: publicClient,
		address: paymasterAddress,
		abi: parseAbi([
			"function getPrice() view returns (uint192)",
			"function priceMarkup() view returns (uint32)",
			"function refundPostOpCost() view returns (uint)",
		]),
	});
 
	const requiredGas =
		userOperation.verificationGasLimit +
		userOperation.callGasLimit +
		(userOperation.paymasterVerificationGasLimit || 0n) +
		(userOperation.paymasterPostOpGasLimit || 0n) +
		userOperation.preVerificationGas;
 
	const requiredPreFund = requiredGas * userOperation.maxFeePerGas;
 
	// fetch onchain info
	const markup = BigInt(await erc20Paymaster.read.priceMarkup());
	const refundPostOpCost = BigInt(await erc20Paymaster.read.refundPostOpCost());
	const tokenPrice = BigInt(await erc20Paymaster.read.getPrice());
 
	const maxFeePerGas = userOperation.maxFeePerGas;
 
	return (
		((requiredPreFund + refundPostOpCost * maxFeePerGas) *
			markup *
			tokenPrice) /
		(BigInt(1e18) * BigInt(1e6))
	);
};

Estimating Gas Cost

The helper function below shows an example on how to accurately estimate the gas cost of a UserOperation when using Pimlico's paymasters. The function expects a valid and signed UserOperation.

// Returns the cost of the UserOperation (denominated in token)
export const estimateCost = async ({
	userOperation,
	publicClient,
	paymasterAddress,
}: EstimateParams) => {
	const beneficiary = privateKeyToAddress(generatePrivateKey());
 
	const erc20Paymaster = getContract({
		address: paymasterAddress,
		abi: parseAbi(["function token() view returns (address)"]),
		client: publicClient,
	});
	const erc20Token = await erc20Paymaster.read.token();
 
	const multicall3Abi = parseAbi([
		"struct Result { bool success; bytes returnData; }",
		"struct Call3 { address target; bool allowFailure; bytes callData; }",
		"function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData)",
	]);
 
	const entrypointAbi = parseAbi([
		"struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; bytes32 accountGasLimits; uint256 preVerificationGas; bytes32 gasFees; bytes paymasterAndData; bytes signature; }",
		"function handleOps(PackedUserOperation[] calldata ops, address beneficiary)",
		"error FailedOp(uint256 opIndex, string reason)",
		"error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner)",
		"error PostOpReverted(bytes returnData)",
		"error SignatureValidationFailed(address aggregator)",
	]);
	const handleOpsData = encodeFunctionData({
		abi: entrypointAbi,
		functionName: "handleOps",
		args: [[getPackedUserOperation(userOperation)], beneficiary],
	});
 
	const getTokenBalanceData = encodeFunctionData({
		abi: parseAbi(["function balanceOf(address) returns (uint)"]),
		args: [userOperation.sender],
	});
 
	const multicallPayload = encodeFunctionData({
		abi: multicall3Abi,
		functionName: "aggregate3",
		args: [
			[
				{
					target: erc20Token,
					callData: getTokenBalanceData,
					allowFailure: true,
				},
				{
					target: ENTRYPOINT_ADDRESS_V07,
					callData: handleOpsData,
					allowFailure: true,
				},
				{
					target: erc20Token,
					callData: getTokenBalanceData,
					allowFailure: true,
				},
			],
		],
	});
 
	const response = await publicClient.call({
		to: "0xca11bde05977b3631167028862be2a173976ca11",
		data: multicallPayload,
		gasPrice: await publicClient.getGasPrice(),
	});
 
	if (!response.data) {
		throw new Error("Failed to estimate erc20Paymaster's gas cost");
	}
 
	const [balBefore, handleOps, balAfter] = decodeAbiParameters(
		multicall3Abi[0].outputs,
		response.data,
	)[0];
 
	if (!balBefore.success || !balAfter.success) {
		throw new Error("Failed to estimate erc20Paymaster's gas cost");
	}
 
	if (!handleOps.success) {
		const parsedError = decodeErrorResult({
			abi: entrypointAbi,
			data: handleOps.returnData,
		});
 
		throw new Error(
			`Failed to estimate erc20Paymaster's gas cost due to ${parsedError.args}`,
		);
	}
 
	return hexToBigInt(balBefore.returnData) - hexToBigInt(balAfter.returnData);
};