When swapping ERC20 tokens (like USDC, USDT, DAI), you need to approve the protocol contract to spend your tokens before the actual swap. This guide explains the two-transaction approval flow and common edge cases.
Why Approvals Are Needed
ERC20 tokens require explicit approval before a smart contract can transfer them on your behalf. This is a security feature that prevents contracts from moving your tokens without permission.
Native tokens like ETH don’t require approval - Only ERC20 tokens need the approval step.
Two-Transaction Flow
When depositing ERC20 tokens, LeoKit returns two unsigned transactions :
Approval Transaction - Authorizes the protocol contract to spend your tokens
Deposit Transaction - Transfers tokens to initiate the swap
Both must be sent sequentially, with the approval transaction confirmed before the deposit.
Example: ETH USDC → BTC
Let’s swap 2000 USDC on Ethereum to BTC.
Step 1: Get Quote
const quote = await fetch (
"https://api.leodex.io/leokit/quote?" +
new URLSearchParams ({
from_asset: "ETH.USDC-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ,
to_asset: "BTC.BTC" ,
amount: "2000000000" , // 2000 USDC (6 decimals)
destination: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" ,
origin: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" ,
}),
{ headers: { "Api-Key" : "your_api_key_here" } }
). then (( r ) => r . json ());
Step 2: Generate Deposit (Returns 2 Transactions)
const deposit = await fetch ( "https://api.leodex.io/leokit/deposit" , {
method: "POST" ,
headers: {
"Api-Key" : "your_api_key_here" ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ({
quote_id: quote . quote_id ,
selected_protocol: "thorchain" ,
}),
}). then (( r ) => r . json ());
console . log ( "Number of transactions:" , deposit . unsigned_transactions . length );
// Output: 2
Response Structure:
{
"type" : "EVM" ,
"protocol" : "thorchain" ,
"quote_id" : "01936b4a-7c8e-7890-abcd-ef1234567890" ,
"unsigned_transactions" : [
{
// Transaction #1: Approval
"to" : "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" ,
"data" : "0x095ea7b3..." ,
"value" : "0x0" ,
"chainId" : 1
},
{
// Transaction #2: Deposit
"to" : "0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146" ,
"data" : "0x1fece7b4..." ,
"value" : "0x0" ,
"chainId" : 1
}
]
}
Step 3: Sign & Send Both Transactions
Critical: Wait for the approval transaction to be confirmed before sending the deposit transaction. Sending them simultaneously will cause the deposit to fail.
import { ethers } from "ethers" ;
const provider = new ethers . providers . Web3Provider ( window . ethereum );
const signer = provider . getSigner ();
// Send approval transaction first
const approvalTx = await signer . sendTransaction (
deposit . unsigned_transactions [ 0 ]
);
console . log ( "Approval TX sent:" , approvalTx . hash );
// Wait for confirmation (1-2 blocks recommended)
await approvalTx . wait ();
console . log ( "Approval confirmed!" );
// Then send deposit transaction
const depositTx = await signer . sendTransaction (
deposit . unsigned_transactions [ 1 ]
);
console . log ( "Deposit TX sent:" , depositTx . hash );
const receipt = await depositTx . wait ();
console . log ( "Deposit confirmed!" );
// Save the deposit transaction hash
await fetch ( "https://api.leodex.io/leokit/save-transaction" , {
method: "POST" ,
headers: {
"Api-Key" : "your_api_key_here" ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ({
quote_id: quote . quote_id ,
tx_hash: depositTx . hash ,
}),
});
Understanding Approval Amounts
LeoKit approves the exact swap amount by default. This is more secure than infinite approvals but requires a new approval for each swap.
Exact Amount Approval (Default)
// Approves exactly 2000 USDC
approve (routerAddress, 2000000000 )
Pros:
More secure (limits exposure)
User-friendly (clear intent)
Cons:
Requires approval for every swap
Higher gas costs over time
Infinite Approval (Not Recommended)
Some protocols support infinite approvals, but LeoKit doesn’t use them for security reasons:
// NOT used by LeoKit
approve (routerAddress, type ( uint256 ).max)
USDT Edge Case: Approval Reset
USDT requires allowance reset to zero before changing approval amounts. This is a quirk of the USDT token contract.
If you’re swapping USDT and have an existing approval, LeoKit will return three transactions :
Reset Approval - Set allowance to 0
New Approval - Set allowance to swap amount
Deposit - Execute the swap
// USDT swap may return 3 transactions
const deposit = await fetch ( "https://api.leodex.io/leokit/deposit" , {
method: "POST" ,
headers: {
"Api-Key" : "your_api_key_here" ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ({
quote_id: quote . quote_id ,
selected_protocol: "thorchain" ,
}),
}). then (( r ) => r . json ());
if ( deposit . unsigned_transactions . length === 3 ) {
console . log ( "USDT detected - requires approval reset" );
// 1. Reset to zero
const resetTx = await signer . sendTransaction (
deposit . unsigned_transactions [ 0 ]
);
await resetTx . wait ();
// 2. Approve new amount
const approveTx = await signer . sendTransaction (
deposit . unsigned_transactions [ 1 ]
);
await approveTx . wait ();
// 3. Deposit
const depositTx = await signer . sendTransaction (
deposit . unsigned_transactions [ 2 ]
);
await depositTx . wait ();
}
Checking Current Allowance
You can check if an approval already exists using ethers.js:
const tokenContract = new ethers . Contract (
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" , // USDC address
[ "function allowance(address owner, address spender) view returns (uint256)" ],
provider
);
const currentAllowance = await tokenContract . allowance (
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" , // Your address
"0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146" // Router address
);
console . log ( "Current allowance:" , currentAllowance . toString ());
Gas Optimization
Approvals cost gas. Here are strategies to minimize costs:
If you’re doing multiple swaps of the same token, consider approving a larger amount once instead of per-swap approvals. Note: LeoKit doesn’t currently support this, but you can manually approve higher amounts.
Use EIP-2612 Permits (Advanced)
Some tokens support gasless approvals via EIP-2612 permits. This requires signature-based approvals instead of transactions. Not yet supported by LeoKit.
Submit approval transactions during low gas periods (typically weekends) to save costs.
Error Handling
Common approval errors and solutions:
Error Cause Solution Insufficient allowanceApproval not confirmed yet Wait for approval TX confirmation Execution revertedInsufficient token balance Check token balance includes amount + gas Transaction underpricedGas price too low Increase gas price Nonce too lowTransactions sent out of order Ensure sequential nonce management
Best Practices
Always wait for approval confirmation - Don’t send the deposit transaction until the approval is confirmed on-chain.
Handle USDT specially - Check for 3-transaction responses when swapping USDT.
Show clear UI feedback - Inform users they’ll need to sign 2+ transactions before starting.
Estimate total gas costs - Calculate approval + deposit gas costs and show users upfront.
Support transaction resumption - If approval succeeds but deposit fails, allow users to retry just the deposit.
Next Steps
Complete Swap Flow See the full swap workflow from quote to completion
Error Debugging Debug failed approvals using trace IDs