Celo allows users to pay gas fees in currencies other than the native CELO token. The list of accepted tokens is governed on-chain and maintained in FeeCurrencyDirectory.sol.
To use an alternate fee currency, set its token or adapter address as the feeCurrency property on the transaction object. This field is exclusive to Celo. Leaving it empty defaults to CELO. Note that transactions specifying a non-CELO fee currency cost approximately 50,000 additional gas.
Allowlisted Fee Currencies
The protocol maintains a governable allowlist of smart contract addresses that implement an extension of the ERC20 interface, with additional functions for debiting and crediting transaction fees.
To fetch the current allowlist, call getCurrencies() on the FeeCurrencyDirectory contract, or use celocli:
# Celo Sepolia testnet
celocli network:whitelist --node celo-sepolia
# Celo mainnet
celocli network:whitelist --node https://forno.celo.org
Adapters for Non-18-Decimal Tokens
After Contract Release 11, allowlisted addresses may be adapters rather than full ERC20 tokens. Adapters are used when a token has decimals other than 18 (e.g., USDC and USDT use 6 decimals). The Celo blockchain calculates gas pricing in 18 decimals, so adapters normalize the value.
- For transfers: use the token address as usual.
- For
feeCurrency: use the adapter address.
- For
balanceOf: querying via the adapter returns the balance as if the token had 18 decimals β useful for checking whether an account can cover gas without converting units.
To get the underlying token address for an adapter, call adaptedToken() on the adapter contract.
For more on gas pricing, see Gas Pricing.
Adapter Addresses
Mainnet
Celo Sepolia (Testnet)
Using Fee Abstraction with 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.
Using Fee Abstraction with 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, 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) {
const estimatedGasInHex = await publicClient.estimateGas({
...transaction,
feeCurrency: USDC_ADAPTER_MAINNET,
});
return hexToBigInt(estimatedGasInHex);
}
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, http } 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;
}
If you have any questions, please reach out.