Skip to content

Zero Gas Mass Payment with SBC

This guide demonstrates how to implement gasless mass payments using SBC's custom paymaster and bundler. With this functionality, you can send SBC tokens to multiple recipients in a single transaction without paying gas fees.

Prerequisites

  • Access to SBC's bundler and paymaster services
  • Sufficient SBC tokens in your wallet

Visit the SBC Dashboard to get an API Key as well as SBC on Base Sepolia. Feel free to reach out to us on Telegram to ask any questions.

Contract Addresses

Base Sepolia (Testnet)

  • SBC Token: 0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16

How It Works

A mass payment is a single UserOperation carrying a batch of calls:

  1. One permit call, letting the smart account move SBC on the owner's behalf
  2. One transferFrom call per recipient
  3. Zero gas fees, because SBC's paymaster sponsors the UserOperation

All of it settles in one transaction, for up to 200 recipients.

We'll send 0.01 SBC to each of 2 addresses.

Implementation Steps

1. Imports and setup

import {
  createPublicClient,
  createWalletClient,
  custom,
  encodeFunctionData,
  erc20Abi,
  http,
  parseSignature,
  parseUnits,
  type Hex,
} from "viem";
import { baseSepolia } from "viem/chains";
import { entryPoint07Address } from "viem/account-abstraction";
import { createPaymasterClient } from "viem/account-abstraction";
import { toSimpleSmartAccount } from "permissionless/accounts";
import { createSmartAccountClient } from "permissionless";
import { Token } from "@uniswap/sdk-core";
 
// Helper that builds the EIP-2612 permit signature.
// Source: https://github.com/stablecoinxyz/masspay/blob/main/src/lib/masspay.ts#L232
import { getPermitSignature } from "./lib/masspay";
 
const chain = baseSepolia;
const SBC_TOKEN_ADDRESS = "0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16" as Hex;
const SBC_DECIMALS = 6;
 
// getPermitSignature reads the token's name and address to build the EIP-712 domain
const SBC = new Token(chain.id, SBC_TOKEN_ADDRESS, SBC_DECIMALS, "SBC", "Stable Coin");
 
// Visit https://dashboard.stablecoin.xyz to get an API key
const PAYMASTER_API_URL = `https://api.aa.stablecoin.xyz/rpc/v1/baseSepolia/${process.env.SBC_PAYMASTER_API_KEY}`;
 
const publicClient = createPublicClient({
  chain,
  transport: http(),
});

2. Create a SimpleAccount smart account from your wallet (owner)

const [ownerAddress] = await (window as any).ethereum.request({
  method: "eth_requestAccounts",
});
 
const owner = createWalletClient({
  account: ownerAddress as Hex,
  chain,
  transport: custom((window as any).ethereum),
});
 
const simpleAccount = await toSimpleSmartAccount({
  client: publicClient,
  owner,
  entryPoint: {
    address: entryPoint07Address,
    version: "0.7",
  },
});

3. Create a Paymaster Client from our Paymaster URL

const pmClient = createPaymasterClient({
  transport: http(PAYMASTER_API_URL),
});

4. Create the Smart Account Bundler Client with our Paymaster Client

const smartAccountClient = createSmartAccountClient({
  account: simpleAccount,
  chain,
  bundlerTransport: http(PAYMASTER_API_URL),
  paymaster: pmClient,
  userOperation: {
    estimateFeesPerGas: async () => {
      const gasPrice = await publicClient.getGasPrice();
      return {
        maxFeePerGas: gasPrice,
        maxPriorityFeePerGas: gasPrice,
      };
    },
  },
});

5. Prepare the calldata

const txs = [
  { to: "0x124b082e8DF36258198da4Caa3B39c7dFa64D9cE" as Hex, value: 0.01 },
  { to: "0xB5f6fECd59dAd3d5bA4Dfe8FcCA6617CE71B99f9" as Hex, value: 0.01 },
];
 
const txnBigInts = txs.map((tx) => ({
  to: tx.to,
  value: parseUnits(tx.value.toString(), SBC_DECIMALS),
}));
 
// Build the calls array, sending from the owner wallet to each `to` address.
const calls = txnBigInts.map((tx) => ({
  to: SBC_TOKEN_ADDRESS,
  data: encodeFunctionData({
    abi: erc20Abi,
    functionName: "transferFrom",
    args: [owner.account.address, tx.to, tx.value],
  }),
}));

6. Compute the permit signature

The smart account is the one submitting the transfers, so the owner permits it to spend the total.

// Sum the total value of the mass pay
const totalValue = txnBigInts.reduce((acc, tx) => acc + tx.value, 0n);
 
// Set a 30 min deadline for the signature (seconds since epoch)
const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
 
// The SimpleAccount's (counterfactual) address is the spender
const senderAddress = simpleAccount.address;
 
const signature = await getPermitSignature(
  chain,
  owner,
  SBC,
  owner.account.address,
  senderAddress,
  totalValue,
  deadline,
);
 
const { r, s, v } = parseSignature(signature);
 
// Encode the permit call and prepend it to the batch
const permitData = encodeFunctionData({
  abi: erc20PermitAbi,
  functionName: "permit",
  args: [owner.account.address, senderAddress, totalValue, deadline, v, r, s],
});
 
calls.unshift({
  to: SBC_TOKEN_ADDRESS,
  data: permitData,
});

7. Send the batch call with our Smart Account Client

const userOpHash = await smartAccountClient.sendUserOperation({ calls });
 
const receipt = await smartAccountClient.waitForUserOperationReceipt({
  hash: userOpHash,
});
 
console.log(`User Op Hash: ${receipt.userOpHash}`);

Performance Considerations

  • Gas Limits: Mass payments are gas intensive as the information needed (addresses) take up room in the calldata. Each chain has its own maximum gas per block.
  • Transaction Limits: The maximum number of recipients in a single batch is 200.

Next Steps