Skip to main content
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:
  1. Approval Transaction - Authorizes the protocol contract to spend your tokens
  2. 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
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:
  1. Reset Approval - Set allowance to 0
  2. New Approval - Set allowance to swap amount
  3. 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.
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:
ErrorCauseSolution
Insufficient allowanceApproval not confirmed yetWait for approval TX confirmation
Execution revertedInsufficient token balanceCheck token balance includes amount + gas
Transaction underpricedGas price too lowIncrease gas price
Nonce too lowTransactions sent out of orderEnsure 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