Skip to main content
This guide shows how to send transactions that pay gas fees in ERC20 tokens instead of CELO. For background on how fee abstraction works, see the Overview.

Using viem

We recommend viem, which has native support for the feeCurrency field. Ethers.js and web3.js do not currently support this field.

1. Estimate the Gas Fee

Before sending, estimate the transaction fee so the UI can reserve that amount and prevent users from trying to transfer more than their available balance.
The gas price returned from the RPC is always expressed in 18 decimals, regardless of the fee currency.
Use the adapter address (for USDC/USDT) or token address (for USDm, EURm, BRLm) as the feeCurrency value when estimating.
import { createPublicClient, formatEther, hexToBigInt, http } from "viem";
import { celo } from "viem/chains";

const USDC_ADAPTER_MAINNET = "0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B";

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const transaction = {
  from: "0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D",
  to: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
  data: "0xa9059cbb000000000000000000000000ccc9576f841de93cd32bee7b98fe8b9bd3070e3d00000000000000000000000000000000000000000000000000000000000f4240",
  feeCurrency: USDC_ADAPTER_MAINNET,
};

async function getGasPriceInUSDC() {
  const priceHex = await publicClient.request({
    method: "eth_gasPrice",
    params: [USDC_ADAPTER_MAINNET],
  });
  return hexToBigInt(priceHex);
}

async function estimateGasInUSDC(transaction) {
  return await publicClient.estimateGas({
    ...transaction,
    feeCurrency: USDC_ADAPTER_MAINNET,
  });
}

async function main() {
  const gasPriceInUSDC = await getGasPriceInUSDC();
  const estimatedGas = await estimateGasInUSDC(transaction);

  // Total fee the user must reserve before transferring
  const transactionFeeInUSDC = formatEther(gasPriceInUSDC * estimatedGas).toString();
  return transactionFeeInUSDC;
}

2. Prepare the Transaction

Set feeCurrency to the adapter address (USDC/USDT) or token address (USDm, EURm, BRLm). Use transaction type 123 (0x7b), which is CIP-64 compliant.
let tx = {
  // ... other transaction fields
  feeCurrency: "0x2f25deb3848c207fc8e0c34035b3ba7fc157602b", // USDC Adapter address
  type: "0x7b",
};

3. Send the Transaction

The example below transfers 1 USDC, subtracting the estimated fee from the transfer amount so the sender’s full balance is not over-spent.
import { createWalletClient, encodeFunctionData, http, parseEther } from "viem";
import { celo } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { stableTokenAbi } from "@celo/abis";

const account = privateKeyToAccount("0x432c...");

const client = createWalletClient({
  account,
  chain: celo,
  transport: http(),
});

const USDC_ADAPTER_MAINNET = "0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B";
const USDC_MAINNET = "0xcebA9300f2b948710d2653dD7B07f33A8B32118C";

async function calculateTransactionFeesInUSDC(transaction) {
  const gasPriceInUSDC = await getGasPriceInUSDC();
  const estimatedGas = await estimateGasInUSDC(transaction);
  return gasPriceInUSDC * estimatedGas;
}

async function send(amountInWei) {
  const to = USDC_MAINNET;

  const data = encodeFunctionData({
    abi: stableTokenAbi,
    functionName: "transfer",
    args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", amountInWei],
  });

  const transactionFee = await calculateTransactionFeesInUSDC({ to, data });

  // Subtract the fee from the amount so the sender isn't over-spending
  const tokenReceivedByReceiver = parseEther("1") - transactionFee;

  const dataAfterFeeCalculation = encodeFunctionData({
    abi: stableTokenAbi,
    functionName: "transfer",
    args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", tokenReceivedByReceiver],
  });

  const hash = await client.sendTransaction({
    ...{ to, data: dataAfterFeeCalculation },
    feeCurrency: USDC_ADAPTER_MAINNET,
  });

  return hash;
}

Using Celo CLI

Transfer 1 USDC using USDC as the fee currency via celocli:
celocli transfer:erc20 \
  --erc20Address 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
  --from 0x22ae7Cf4cD59773f058B685a7e6B7E0984C54966 \
  --to 0xDF7d8B197EB130cF68809730b0D41999A830c4d7 \
  --value 1000000 \
  --gasCurrency 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
  --privateKey [PRIVATE_KEY]
When using USDC or USDT, use the adapter address (not the token address) to avoid inaccuracies caused by their 6-decimal precision.
If you have any questions, please reach out.