Skip to content

Design

The most important design decision for Pimlico's ERC20Paymaster was permissionlessness — you do not have to interact with any hosted APIs to leverage the paymaster. This has the tradeoff however that token approvals to the paymaster cannot be done during the execution phrase of the UserOperation, and must rather be done during the validation phase or in a previous UserOperation. We felt like this was a good tradeoff to make, because token approvals should only be a one-time friction for users under normal circumstances.

It is important to note that while Pimlico's paymaster is permissionless, it is not decentralized. The owner of the paymaster is able to do a couple very limited actions. These are further explained in the Admin functions section of this page. It was however designed so it can easily be made decentralized in the future.

Oracle

The ERC20 paymaster leverages Chainlink for its price oracles, and uses a combination of the ERC20 Token to USD and Native Token to USD prices to calculate the ERC-20 Token to Native Token price. This means that the paymaster is easily deployable on any chain for any token that has Chainlink support, but it also means that if Chainlink is compromised, the price can be arbitrarily different from the real token price for the end user.

Paymaster-specific functions

Paymasters under the ERC-4337 specification must implement two functions.

validatePaymasterUserOp allows the paymaster to verify whether the paymaster is willing to pay for the User Operation with custom logic. The paymaster is restricted during this phrase in what it can do. Most importantly, it must comply with the banned opcode and banned external storage access rules of the ERC. For this reason, actions like fetching oracle price state are not possible as they access external storage that is not associated with the account.

postOp is called by the EntryPoint on the paymaster after making the main execution call if any context is returned by the validatePaymasterUserOp function. Since our paymaster always returns context, this will always be called.

Paymaster Modes

This paymaster has four modes. It allows the user to simply pay for themselves, but also allows the selection of a guarnator who can front the ERC-20 token fees during validation, allowing the user to approve tokens to the paymaster or fetch / claim tokens if they do not already have any. For each mode, it is possible to set a ERC-20 token spend limit to protect against sudden price fluctuations or oracle manipulation.

Mode 0

  • The user (sender) pays for gas fees with the ERC-20 token.
  • paymasterData is empty

Mode 1

  • The user (sender) pays for gas fees with the ERC-20 token,
  • There is a limit to the amount of ERC-20 tokens that can be taken from the user for the user opertion.
  • paymasterData: "0x01" + token spend limit (32 bytes)

Mode 2

  • A guarantor fronts the ERC-20 token gas fees during validation, and expects the user to be able to pay the actual cost during the postOp phase and get refunded. Otherwise the guarantor is liable.
  • paymasterData: "0x02" + guarantor address (20 bytes) + validUntil (6 bytes) + validAfter (6 bytes) + guarantor signature (dynamic bytes)

Mode 3

  • A guarantor fronts the ERC-20 token gas fees during validation, and expects the user to be able to pay the actual cost during the postOp phase and get refunded. Otherwise the guarantor is liable.
  • There is a limit to the amount of ERC-20 tokens that can be taken from the user/guarantor for the user opertion.
  • paymasterData: "0x03" + token spend limit (32 bytes) + guarantor address (20 bytes) + validUntil (6 bytes) + validAfter (6 bytes) + guarantor signature (dynamic bytes)

Creating a valid guarantor signature

The guarantor address can either be a EOA or Smart Contract.

  • If EOA, verification is done through ECDSA signature recovery.
  • If Smart Contract, verification is done through ERC-1271.

EOA Guarantor Signature

The paymaster validates a userOperation by checking if the guarantor signed the associated hash generated by the userOperation, validUntil, validAfter, and tokenLimit.

index.ts
import { erc20Paymaster }  from "./erc20Paymaster.ts"
import { getPackedUserOperation } from "permissionless/utils"
import { sign, privateKeyToAddress } from "viem/accounts"
import { getContract, encodePacked } from "viem"
 
const guarantorPrivateKey = "0x..."
const userOperation = ...
 
const validAfter = 0
const validUntil = (Date.now() / 1000) + 3600 // valid for 1 hour
 
// getting the hash to sign
const hash = await erc20Paymaster.read.getHash([
    getPackedUserOperation(userOperation),
    validUntil,
    validAfter,
    0n,
])
 
// signing the hash
const { r, s, v } = await sign({ hash, privateKey: guarantorPrivateKey })
const signature = encodePacked(
    ["bytes32", "bytes32", "uint8"],
    [r, s, Number(v)]
)
 
// creating paymaster and data for a Mode2 sponsor
const mode2PaymasterData = encodePacked(
    ["bytes1", "address", "bytes6", "bytes6", "bytes"],
    [
        "0x02",
        privateKeyToAddress(guarantorPrivateKey),
        toHex(validUntil, { size: 6 }),
        toHex(validAfter, { size: 6 }),
        signature,
    ],
)
 
// append paymaster data to userOperation
const sponsoredUserOperation = {
    ...userOperation
    paymaster: erc20Paymaster.address,
    paymasterData: mode2PaymasterData,
}

Smart Contract Guarantor Signature

ERC-1271 is a standard way to verify a signature when the account is a smart contract. The smart contract needs to implement the following interface to be compatible. More details can be found in the original EIP.

IERC1271.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
/**
 * @dev Interface of the ERC-1271 standard signature validation method for
 * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271.
 */
interface IERC1271 {
    /**
     * @dev Checks whether the signature is valid for the provided data
     * @param hash      Hash of the data to be signed
     * @param signature Signature byte array associated with _data
     */
    function isValidSignature(
        bytes32 hash,
        bytes memory signature
    ) external view returns (bytes4 magicValue);
}