Skip to main content
Before executing a transfer with an ERC20 token, you must approve the LI.FI contract to spend tokens on behalf of the user. This page explains when approval is needed and how to handle it correctly.

When Is Approval Needed?

Token TypeApproval RequiredHow to Identify
Native token (ETH, MATIC, etc.)NoAddress is 0x0000000000000000000000000000000000000000
ERC20 token (USDC, USDT, etc.)YesAny other address

Decision Logic

IF fromToken.address === "0x0000000000000000000000000000000000000000"
  → Skip approval (native token)

ELSE
  → Check allowance and approve if needed

Step 1: Identify the Spender

The spender address comes from the quote response:
{
  "estimate": {
    "approvalAddress": "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE"
  }
}
Always use estimate.approvalAddress from the quote response. Never hardcode spender addresses - they may vary by route or bridge.

Step 2: Check Current Allowance

Call the allowance function on the token contract:

Function Signature

function allowance(address owner, address spender) external view returns (uint256)

JavaScript Example

import { createPublicClient, http, erc20Abi } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http()
});

async function checkAllowance(tokenAddress, ownerAddress, spenderAddress) {
  const allowance = await client.readContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [ownerAddress, spenderAddress]
  });
  
  return allowance;
}

// Usage
const currentAllowance = await checkAllowance(
  quote.action.fromToken.address,  // Token contract
  quote.action.fromAddress,         // User's wallet
  quote.estimate.approvalAddress    // LI.FI spender
);

Python Example

from web3 import Web3

# ERC20 ABI (minimal)
ERC20_ABI = [
    {
        "constant": True,
        "inputs": [
            {"name": "owner", "type": "address"},
            {"name": "spender", "type": "address"}
        ],
        "name": "allowance",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    }
]

def check_allowance(web3, token_address, owner, spender):
    contract = web3.eth.contract(address=token_address, abi=ERC20_ABI)
    return contract.functions.allowance(owner, spender).call()

Step 3: Compare and Decide

const fromAmount = BigInt(quote.action.fromAmount);
const currentAllowance = await checkAllowance(...);

if (currentAllowance >= fromAmount) {
  // Sufficient allowance - proceed to execution
  console.log('Allowance sufficient, skipping approval');
} else {
  // Need to approve
  console.log('Approval needed');
  await sendApproval(...);
}

Step 4: Send Approval Transaction

Function Signature

function approve(address spender, uint256 amount) external returns (bool)

Approval Amount Strategies

StrategyAmountProsCons
ExactfromAmountMost secureRequires approval for every transfer
Unlimitedtype(uint256).maxOne-time approvalHigher risk if contract is compromised
BufferfromAmount * 2Balance of security and UXMay still need re-approval
For AI agents, exact approval is recommended for security. For user-facing apps, consider letting users choose.

JavaScript Example

import { createWalletClient, http, erc20Abi, maxUint256 } from 'viem';

async function approveToken(walletClient, tokenAddress, spenderAddress, amount) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [spenderAddress, amount]
  });
  
  // Wait for transaction confirmation
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  
  if (receipt.status !== 'success') {
    throw new Error('Approval transaction failed');
  }
  
  return receipt;
}

// Exact approval
await approveToken(
  walletClient,
  quote.action.fromToken.address,
  quote.estimate.approvalAddress,
  BigInt(quote.action.fromAmount)
);

// Or unlimited approval
await approveToken(
  walletClient,
  quote.action.fromToken.address,
  quote.estimate.approvalAddress,
  maxUint256
);

Python Example

def approve_token(web3, token_address, spender, amount, private_key):
    contract = web3.eth.contract(address=token_address, abi=ERC20_ABI)
    
    # Build transaction
    tx = contract.functions.approve(spender, amount).build_transaction({
        'from': account.address,
        'nonce': web3.eth.get_transaction_count(account.address),
        'gas': 100000,
        'gasPrice': web3.eth.gas_price
    })
    
    # Sign and send
    signed = web3.eth.account.sign_transaction(tx, private_key)
    tx_hash = web3.eth.send_raw_transaction(signed.rawTransaction)
    
    # Wait for confirmation
    receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
    
    if receipt['status'] != 1:
        raise Exception('Approval failed')
    
    return receipt

Complete Approval Flow

async function ensureApproval(quote, walletClient, publicClient) {
  const { fromToken, fromAmount, fromAddress } = quote.action;
  const { approvalAddress } = quote.estimate;
  
  // Step 1: Check if native token
  const NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000';
  if (fromToken.address.toLowerCase() === NATIVE_ADDRESS) {
    console.log('Native token - no approval needed');
    return null;
  }
  
  // Step 2: Check current allowance
  const currentAllowance = await publicClient.readContract({
    address: fromToken.address,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [fromAddress, approvalAddress]
  });
  
  // Step 3: Compare
  const requiredAmount = BigInt(fromAmount);
  if (currentAllowance >= requiredAmount) {
    console.log('Sufficient allowance exists');
    return null;
  }
  
  // Step 4: Send approval
  console.log('Sending approval transaction...');
  const hash = await walletClient.writeContract({
    address: fromToken.address,
    abi: erc20Abi,
    functionName: 'approve',
    args: [approvalAddress, requiredAmount]
  });
  
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  console.log(`Approval confirmed: ${receipt.transactionHash}`);
  
  return receipt;
}

Common Mistakes

Always use quote.estimate.approvalAddress, not a hardcoded address. Different routes may use different contracts.
The main transaction will fail if sent before the approval is confirmed. Always wait for the approval receipt.
Ensure your wallet is connected to the source chain (fromChain) when sending the approval.
Approval transactions typically need 50,000-100,000 gas. Use a reasonable gas limit.
USDT requires setting allowance to 0 before setting a new non-zero value. Check current allowance and reset if needed.
// USDT special handling
if (tokenSymbol === 'USDT' && currentAllowance > 0) {
  await approveToken(tokenAddress, spender, 0n); // Reset to 0
  await approveToken(tokenAddress, spender, amount); // Then set new amount
}

Approval Checklist

Before executing a transfer, verify:
  • fromToken.address is NOT 0x0000...0000 (native token)
  • Retrieved approvalAddress from quote response
  • Checked current allowance(owner, spender)
  • Compared allowance to fromAmount
  • Sent approval tx if allowance < fromAmount
  • Waited for approval confirmation
  • Connected to correct chain (fromChain)