Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 58 additions & 107 deletions tooling/overview/fee-abstraction.mdx
Original file line number Diff line number Diff line change
@@ -1,92 +1,93 @@
---
title: Implementing Fee Abstraction in Wallets
og:description: How to allow your wallet users to pay for gas fee using alternate fee currencies
og:description: How to allow your wallet users to pay for gas fees using alternate fee currencies
sidebarTitle: "Fee Abstraction"
---

Celo allows paying gas fees in currency other than the native currency. The tokens that can be used to pay gas fees is controlled via governance and the list of tokens allowed is maintained in [**FeeCurrencyDirectory.sol**](/contracts/core-contracts).
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**](/contracts/core-contracts).

This works by specifying a token/adapter address as a value for the `feeCurrency` property in the transaction object. The `feeCurrency` property in the transaction object is exclusive to Celo and allows paying gas fees using assets other than the native currency of the network.
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.

The below steps describe how wallets can implement the alternate fee currency feature in order to allow users to use alternate assets in the user's wallet to pay for gas fees.

## Fee Currency Field

The protocol maintains a governable allowlist of smart contract addresses which can be used to pay for transaction fees. These smart contracts implement an extension of the ERC20 interface, with additional functions that allow the protocol to debit and credit transaction fees. When creating a transaction, users can specify the address of the currency they would like to use to pay for gas via the `feeCurrency` field. Leaving this field empty will result in the native currency, CELO, being used. Note that transactions that specify non-CELO gas currencies will cost approximately 50k additional gas.

## Allowlisted Gas Fee Addresses
---

To obtain a list of the gas fee addresses that have been allowlisted using [Celo's Governance Process](/home/protocol/governance/overview), you can run the `getCurrencies()` method on the `FeeCurrencyDirectory` contract. All other notable Mainnet core smart contracts are listed [here](/contracts/core-contracts#celo-mainnet).
## Allowlisted Fee Currencies

You can also query the available tokens with `celocli`:
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`:
```bash
# Celo Sepolia testnet
celocli network:whitelist --node celo-sepolia

# Celo mainnet
celocli network:whitelist --node https://forno.celo.org
```

### Tokens with Adapters
---

After Contract Release 11, addresses in the allowlist are no longer guaranteed to be full ERC20 tokens and can now also be [adapters](https://github.com/celo-org/celo-monorepo/blob/release/core-contracts/11/packages/protocol/contracts-0.8/stability/FeeCurrencyAdapter.sol). Adapters are allowlisted in-lieu of tokens in the scenario that a ERC20 token has decimals other than 18 (e.g. USDT and USDC).
## Adapters for Non-18-Decimal Tokens

The Celo Blockchain natively works with 18 decimals when calculating gas pricing, so adapters are needed to normalize the decimals for tokens that use a different one. Some stablecoins use 6 decimals as a standard.
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.

Transactions with those ERC20 tokens are performed as usual (using the token address), but when paying gas currency with those ERC20 tokens, the adapter address should be used. This adapter address is also the one that should be used when querying [Gas Price Minimum](/legacy/protocol/transaction/gas-pricing).
- **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.

Adapters can also be used to query `balanceOf(address)` of an account, but it will return the balance as if the token had 18 decimals and not the native ones. This is useful to calculate if an account has enough balance to cover gas after multiplying `gasPrice * estimatedGas` without having to convert back to the token's native decimals.
To get the underlying token address for an adapter, call `adaptedToken()` on the adapter contract.

To learn more about gas pricing, see [Gas Pricing](/legacy/protocol/transaction/gas-pricing).
For more on gas pricing, see [Gas Pricing](/legacy/protocol/transaction/gas-pricing).

#### Adapters by network
### Adapter Addresses

##### Mainnet
#### Mainnet

| Name | Token | Adapter |
| ------ | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Name | Token Address | Adapter Address |
| ------ | ------------- | --------------- |
| `USDC` | [`0xcebA9300f2b948710d2653dD7B07f33A8B32118C`](https://celoscan.io/address/0xcebA9300f2b948710d2653dD7B07f33A8B32118C#code) | [`0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B`](https://celoscan.io/address/0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B#code) |
| `USDT` | [`0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e`](https://celoscan.io/address/0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e#code) | [`0x0e2a3e05bc9a16f5292a6170456a710cb89c6f72`](https://celoscan.io/address/0x0e2a3e05bc9a16f5292a6170456a710cb89c6f72#code) |

##### Celo Sepolia (testnet)
#### Celo Sepolia (Testnet)

| Name | Token | Adapter |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Name | Token Address | Adapter Address |
| ------ | ------------- | --------------- |
| `USDC` | [`0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B`](https://sepolia.celoscan.io/address/0x2f25deb3848c207fc8e0c34035b3ba7fc157602b#code) | [`0x4822e58de6f5e485eF90df51C41CE01721331dC0`](https://sepolia.celoscan.io/address/0x4822e58de6f5e485eF90df51C41CE01721331dC0#code) |

## Using Fee Abstraction with Celo CLI
---

Transfer 1 USDC using USDC as fee currency, with the [`celocli`](/cli) using the `--gasCurrency` flag
## Using Fee Abstraction with Celo CLI

Transfer 1 USDC using USDC as the fee currency via [`celocli`](/cli):
```bash
celocli transfer:erc20 --erc20Address 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B --from 0x22ae7Cf4cD59773f058B685a7e6B7E0984C54966 --to 0xDF7d8B197EB130cF68809730b0D41999A830c4d7 --value 1000000 --gasCurrency 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B --privateKey [PRIVATE_KEY]
celocli transfer:erc20 \
--erc20Address 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
--from 0x22ae7Cf4cD59773f058B685a7e6B7E0984C54966 \
--to 0xDF7d8B197EB130cF68809730b0D41999A830c4d7 \
--value 1000000 \
--gasCurrency 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
--privateKey [PRIVATE_KEY]
```

When using USDC or USDC for fee abstraction, you have to use the adapter address instead of the token address. This is necessary to avoid inaccuracies due to their low number of decimals (6 compared to 18 for the other tokens).
When using USDC or USDT, use the adapter address (not the token address) to avoid inaccuracies caused by their 6-decimal precision.

## Enabling Transactions with ERC20 Token as fee currency
---

## Using Fee Abstraction with viem

We recommend using [viem](https://viem.sh/) as it has support for the `feeCurrency` field in the transaction required for sending transactions where the gas fees will be paid in ERC20 tokens. Ethers.js and web.js currently don't support `feeCurrency`.
We recommend [viem](https://viem.sh/), which has native support for the `feeCurrency` field. Ethers.js and web3.js do not currently support this field.

### Estimating gas price
### 1. Estimate the Gas Fee

To estimate gas price use the token address (in case of USDm, EURm and BRLm) or the adapter address (in case of USDC and USDT) as the value for feeCurrency field in the transaction.
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.

<Note>
The Gas Price Minimum value returned from the RPC has to be interpreted in 18 decimals.
The gas price returned from the RPC is always expressed in 18 decimals, regardless of the fee currency.
</Note>

Estimating gas price is important because if the user is trying to transfer the entire balance in an asset and using the same asset to pay for gas fees, the user shouldn't be able to transfer the entire amount as a small portion will be utilized to pay for gas fees.

Example: If the user has 10 USDC and is trying to transfer the entire 10 USDC and chooses to use USDC as the currency to pay for gas, the user shouldn't be allowed to transfer the entire 10 USDC as a small portion has to be used for gas fees.

The following code snippet calculates the transaction fee (in USDC) for a USDC transfer transaction.

Use the adapter address (for USDC/USDT) or token address (for USDm, EURm, BRLm) as the `feeCurrency` value when estimating.
```js
import { createPublicClient, hexToBigInt, http } from "viem";
import { celo } from "viem/chains";

// USDC is 6 decimals and hence requires the adapter address instead of the token address
const USDC_ADAPTER_MAINNET = "0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B";

const publicClient = createPublicClient({
Expand All @@ -106,46 +107,30 @@ async function getGasPriceInUSDC() {
method: "eth_gasPrice",
params: [USDC_ADAPTER_MAINNET],
});

return hexToBigInt(priceHex);
}

async function estimateGasPriceInUSDC(transaction) {
async function estimateGasInUSDC(transaction) {
const estimatedGasInHex = await publicClient.estimateGas({
...transaction,

// just in case the transaction itself does not have feeCurrency property in it.
feeCurrency: USDC_ADAPTER_MAINNET,
});

return hexToBigInt(estimatedGasInHex);
}

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

const estimatedGasPrice = await estimateGasInUSDC(transaction);

/*
Transaction fee in USDC to perform the above transaction.
This amount should not be transferrable in case the user tries to transfer the entire amount.
*/
const transactionFeeInUSDC = formatEther(
gasPriceInUSDC * estimatedGasPrice,
).toString();

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

### Preparing a transaction

When preparing a transaction that uses ERC20 token for gas fees, use the token address (in case of USDm, EURm and BRLm) or the adapter address (in case of USDC and USDT) as the value for `feeCurrency` field in the transaction.

The recommended transaction `type` is `123`, which is a CIP-64 compliant transaction read more about it [here](/legacy/protocol/transaction/transaction-types).

Here is how a transaction would look like when using USDC as a medium to pay for gas fees.
### 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](/legacy/protocol/transaction/transaction-types) compliant.
```js
let tx = {
// ... other transaction fields
Expand All @@ -154,95 +139,61 @@ let tx = {
};
```

<Note>
To get details about the underlying token of the adapter you can call `adaptedToken` function on the adapter address, which will return the underlying token address.
</Note>

### Sending transaction using Fee Abstraction

Sending transaction with fee currency other than the native currency of the network is pretty straightforward all you need to do is set the `feeCurrency` property in the transaction object with the address of the token/adapter you want to use to pay for gas fees.

The below code snippet demonstrates transferring 1 USDC using USDC as gas currency.
### 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.
```js
import { createWalletClient, http } from "viem";
import { celo } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { stableTokenAbi } from "@celo/abis";

// Creating account from private key, you can choose to do it any other way.
const account = privateKeyToAccount("0x432c...");

// WalletClient can perform transactions.
const client = createWalletClient({
account,

// Passing chain is how viem knows to try serializing tx as cip42.
chain: celo,
transport: http(),
});

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

/*
The UI of the wallet should calculate the transaction fees, show it and consider the amount to not be part of the asset that the user i.e the amount corresponding to transaction fees should not be transferrable.
*/
async function calculateTransactionFeesInUSDC(transaction) {
// Implementation of getGasPriceInUSDC is in the above code snippet
const gasPriceInUSDC = await getGasPriceInUSDC();

// Implementation of estimateGasInUSDC is in the above code snippet
const estimatedGasPrice = await estimateGasInUSDC(transaction);

return gasPriceInUSDC * estimatedGasPrice;
const estimatedGas = await estimateGasInUSDC(transaction);
return gasPriceInUSDC * estimatedGas;
}

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

// Data to perform an ERC20 transfer
const data = encodeFunctionData({
abi: stableTokenAbi,
functionName: "transfer",
args: [
"0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D",
// Different tokens can have different decimals, USDm (18), USDC (6)
amountInWei,
],
args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", amountInWei],
});

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

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

/*
Now the data has to be encode again but with different transfer value because the receiver receives the amount minus the transaction fee.
*/
const dataAfterFeeCalculation = encodeFunctionData({
abi: stableTokenAbi,
functionName: "transfer",
args: [
"0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D",
// Different tokens can have different decimals, USDm (18), USDC (6)
tokenReceivedbyReceiver,
],
args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", tokenReceivedByReceiver],
});

// Transaction hash
const hash = await client.sendTransaction({
...{ to, data: dataAfterFeeCalculation },

/*
In case the transaction request does not include the feeCurrency property, the wallet can add it or change it to a different currency based on the user balance.

Notice that we use the USDC_ADAPTER_MAINNET address not the token address this is because at the protocol level only 18 decimals tokens are supported, but USDC is 6 decimals, the adapter acts a unit converter.
*/
feeCurrency: USDC_ADAPTER_MAINNET,
});

return hash;
}
```

---

If you have any questions, please [reach out](https://github.com/celo-org/developer-tooling/discussions/categories/q-a).
Loading