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