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 Type Approval Required How to Identify Native token (ETH, MATIC, etc.) No Address is 0x0000000000000000000000000000000000000000 ERC20 token (USDC, USDT, etc.) Yes Any 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
Strategy Amount Pros Cons Exact fromAmountMost secure Requires approval for every transfer Unlimited type(uint256).maxOne-time approval Higher risk if contract is compromised Buffer fromAmount * 2Balance of security and UX May 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
Using wrong spender address
Always use quote.estimate.approvalAddress, not a hardcoded address. Different routes may use different contracts.
Not waiting for approval confirmation
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.
Insufficient gas for 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 , 0 n ); // Reset to 0
await approveToken ( tokenAddress , spender , amount ); // Then set new amount
}
Approval Checklist
Before executing a transfer, verify:
Related Pages