Skip to main content
LI.FI supports three token approval strategies on EVM chains. In addition to the classic approve() transaction, integrators can use EIP-2612 Permits or Uniswap Permit2 to authorize token transfers via off-chain signatures, reducing the number of on-chain transactions required.
If you use the LI.FI SDK or Widget, Permit2 is handled automatically. This guide is for integrators building directly against the API who want to understand or implement the permit flow themselves.

Overview of Approval Strategies

StrategyOn-chain TransactionsHow It Works
Classic approve()1 approval tx + 1 swap/bridge txUser sends an ERC-20 approve() to the LI.FI Diamond, then submits the swap/bridge transaction.
EIP-2612 Native Permit1 swap/bridge tx onlyUser signs an off-chain EIP-712 message. The signature is submitted alongside the swap calldata to the Permit2Proxy, which calls permit() on the token contract. Only works with tokens that implement EIP-2612.
Uniswap Permit21 one-time approval + 1 swap/bridge tx (signature only)User approves the Permit2 contract once (unlimited). For each subsequent transaction, the user signs an off-chain EIP-712 message authorizing a specific transfer. Works with any ERC-20 token.

Why Permit2?

With the classic approval model, every new dApp interaction requires a separate approve() transaction. Permit2 replaces this with a single, one-time unlimited approval to the canonical Permit2 contract. All subsequent authorizations happen through gasless EIP-712 signatures with per-transfer granularity (exact amount, deadline, nonce).

Architecture

All permit-based flows go through the Permit2Proxy periphery contract, which acts as an intermediary between the user and the LI.FI Diamond: Permit2 Architecture

Key Addresses

ContractAddressNotes
LI.FI Diamond0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaEMost EVM chains. Some networks use a different address — always query GET /v1/chains or check Smart Contract Addresses.
Uniswap Permit20x000000000022D473030F116dDEE9F6B43aC78BA3Most EVM chains. Several networks use a different deployment (e.g. zkSync, Abstract, Lens, Flare, Sophon, XDC) — always read permit2 from GET /v1/chains.
Permit2ProxyChain-specificQuery GET /v1/chains — each chain object includes permit2 and permit2Proxy fields.

Discovering Addresses via the API

const response = await fetch('https://li.quest/v1/chains');
const { chains } = await response.json();

const arbitrum = chains.find((c) => c.id === 42161);
console.log(arbitrum.permit2);      // "0x000000000022D473030F116dDEE9F6B43aC78BA3"
console.log(arbitrum.permit2Proxy); // chain-specific Permit2Proxy address
console.log(arbitrum.diamondAddress); // "0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE"

API Flow: Permit2 (Standard Self-Execute)

This is the most common permit flow. It works with any ERC-20 token on chains where Permit2 is deployed.
1

Get a quote

Request a quote as usual. The response includes estimate.approvalAddress (the Diamond address for classic approve) and the chain metadata you need.
const quote = await fetch('https://li.quest/v1/quote?' + new URLSearchParams({
  fromChain: '42161',
  toChain: '10',
  fromToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum
  toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism
  fromAmount: '10000000', // 10 USDC
  fromAddress: '0xYourWalletAddress',
})).then(r => r.json());
2

Check and set Permit2 allowance (one-time)

The user needs to approve the Permit2 contract (not the Diamond) once. This only needs to happen if the user hasn’t already approved Permit2 for this token.Resolve the Permit2 and Permit2Proxy addresses from the chains API — do not hardcode them, as several networks use non-canonical deployments.
import { createPublicClient, createWalletClient, http, maxUint256, parseAbi } from 'viem';
import { arbitrum } from 'viem/chains';

const publicClient = createPublicClient({ chain: arbitrum, transport: http() });
const walletClient = createWalletClient({ chain: arbitrum, transport: http(), account });

const chainsResponse = await fetch('https://li.quest/v1/chains').then(r => r.json());
const fromChain = chainsResponse.chains.find((c) => c.id === quote.action.fromChainId);
const permit2Address = fromChain.permit2;
const permit2ProxyAddress = fromChain.permit2Proxy;

const erc20Abi = parseAbi([
  'function allowance(address owner, address spender) view returns (uint256)',
  'function approve(address spender, uint256 amount) returns (bool)',
]);

const tokenAddress = quote.action.fromToken.address;

const allowance = await publicClient.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: 'allowance',
  args: [account.address, permit2Address],
});

if (allowance < BigInt(quote.action.fromAmount)) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [permit2Address, maxUint256],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}
3

Read the next available nonce

Permit2 uses unordered nonces. The Permit2Proxy contract provides a nextNonce() helper that finds the next unused nonce for the signer.
const permit2ProxyAbi = parseAbi([
  'function nextNonce(address owner) view returns (uint256)',
  'function callDiamondWithPermit2(bytes diamondCalldata, ((address token, uint256 amount) permitted, uint256 nonce, uint256 deadline) permit, bytes signature) external',
]);

const nonce = await publicClient.readContract({
  address: permit2ProxyAddress,
  abi: permit2ProxyAbi,
  functionName: 'nextNonce',
  args: [account.address],
});
4

Build and sign the PermitTransferFrom message

Construct the EIP-712 typed data for PermitTransferFrom. The spender is the Permit2Proxy (not the Diamond).
const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60); // 30 minutes

const permitTransferFrom = {
  permitted: {
    token: tokenAddress,
    amount: BigInt(quote.action.fromAmount),
  },
  spender: permit2ProxyAddress,
  nonce,
  deadline,
};

const signature = await walletClient.signTypedData({
  account,
  primaryType: 'PermitTransferFrom',
  domain: {
    name: 'Permit2',
    chainId: arbitrum.id,
    verifyingContract: permit2Address,
  },
  types: {
    TokenPermissions: [
      { name: 'token', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    PermitTransferFrom: [
      { name: 'permitted', type: 'TokenPermissions' },
      { name: 'spender', type: 'address' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  message: permitTransferFrom,
});
5

Encode and send the transaction to Permit2Proxy

Wrap the Diamond calldata inside a callDiamondWithPermit2 call targeting the Permit2Proxy, not the Diamond.
import { encodeFunctionData } from 'viem';

const diamondCalldata = quote.transactionRequest.data;

const txData = encodeFunctionData({
  abi: permit2ProxyAbi,
  functionName: 'callDiamondWithPermit2',
  args: [
    diamondCalldata,
    [
      [permitTransferFrom.permitted.token, permitTransferFrom.permitted.amount],
      permitTransferFrom.nonce,
      permitTransferFrom.deadline,
    ],
    signature,
  ],
});

const txHash = await walletClient.sendTransaction({
  to: permit2ProxyAddress,
  data: txData,
  value: BigInt(quote.transactionRequest.value ?? 0),
  gasLimit: BigInt(quote.transactionRequest.gasLimit ?? 0),
});
6

Track the transfer status

Track the transaction status as you normally would using the /status endpoint.
const getStatus = async (txHash) => {
  const result = await fetch(`https://li.quest/v1/status?txHash=${txHash}`);
  return result.json();
};

let status;
do {
  status = await getStatus(txHash);
  if (status.status === 'PENDING') await new Promise(r => setTimeout(r, 5000));
} while (status.status !== 'DONE' && status.status !== 'FAILED');

API Flow: EIP-2612 Native Permit

EIP-2612 permits are only available for tokens that implement the permit() function (e.g., USDC, AAVE, UNI). No prior approve() transaction is needed at all.
Not all tokens support EIP-2612. DAI uses a non-standard permit signature that LI.FI does not currently support. If the token does not implement EIP-2612, fall back to classic approve() or Permit2.

Detecting EIP-2612 Support

There is no on-chain registry or ERC-165 interface for EIP-2612. The only reliable method is to probe the token contract for the required functions. If nonces() and DOMAIN_SEPARATOR() both return successfully, the token supports EIP-2612.
const eip2612DetectAbi = parseAbi([
  'function nonces(address owner) view returns (uint256)',
  'function DOMAIN_SEPARATOR() view returns (bytes32)',
]);

async function supportsEIP2612(tokenAddress: string): Promise<boolean> {
  try {
    await Promise.all([
      publicClient.readContract({
        address: tokenAddress,
        abi: eip2612DetectAbi,
        functionName: 'nonces',
        args: [account.address],
      }),
      publicClient.readContract({
        address: tokenAddress,
        abi: eip2612DetectAbi,
        functionName: 'DOMAIN_SEPARATOR',
      }),
    ]);
    return true;
  } catch {
    return false;
  }
}
Tokens deployed with OpenZeppelin v4.9+ or v5.x also expose eip712Domain() (EIP-5267), which returns all domain fields in a single call. For older tokens, read name(), version(), and DOMAIN_SEPARATOR() separately and recompute the separator to validate it.
1

Get a quote and retrieve diamond calldata

Same as the standard flow: request a quote, then use the transactionRequest.data as your diamond calldata.
2

Read the token's permit nonce

EIP-2612 tokens track nonces per-owner. Read the current nonce from the token contract.
const eip2612Abi = parseAbi([
  'function nonces(address owner) view returns (uint256)',
  'function name() view returns (string)',
  'function version() view returns (string)',
  'function DOMAIN_SEPARATOR() view returns (bytes32)',
]);

const [nonce, name, version] = await Promise.all([
  publicClient.readContract({
    address: tokenAddress, abi: eip2612Abi,
    functionName: 'nonces', args: [account.address],
  }),
  publicClient.readContract({
    address: tokenAddress, abi: eip2612Abi, functionName: 'name',
  }),
  publicClient.readContract({
    address: tokenAddress, abi: eip2612Abi, functionName: 'version',
  }),
]);
3

Sign the EIP-2612 Permit message

The spender is the Permit2Proxy address.
const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60);

const permitSignature = await walletClient.signTypedData({
  account,
  primaryType: 'Permit',
  domain: {
    name,
    version,
    chainId: arbitrum.id,
    verifyingContract: tokenAddress,
  },
  types: {
    Permit: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  },
  message: {
    owner: account.address,
    spender: permit2ProxyAddress,
    value: BigInt(quote.action.fromAmount),
    nonce,
    deadline,
  },
});
4

Encode and send via Permit2Proxy

import { parseSignature, encodeFunctionData } from 'viem';

const { v, r, s } = parseSignature(permitSignature);

const permit2ProxyEip2612Abi = parseAbi([
  'function callDiamondWithEIP2612Signature(address tokenAddress, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s, bytes diamondCalldata) external payable',
]);

const txData = encodeFunctionData({
  abi: permit2ProxyEip2612Abi,
  functionName: 'callDiamondWithEIP2612Signature',
  args: [
    tokenAddress,
    BigInt(quote.action.fromAmount),
    deadline,
    Number(v),
    r,
    s,
    quote.transactionRequest.data,
  ],
});

const txHash = await walletClient.sendTransaction({
  to: permit2ProxyAddress,
  data: txData,
  value: BigInt(quote.transactionRequest.value ?? 0),
});

SDK Usage

The @lifi/sdk handles Permit2 automatically during route execution. No manual signature construction is needed.
import { createConfig, EVM, executeRoute } from '@lifi/sdk';
import { createWalletClient, http } from 'viem';
import { arbitrum } from 'viem/chains';

createConfig({
  integrator: 'your-integrator-id',
  providers: [
    EVM({
      getWalletClient: () => Promise.resolve(walletClient),
    }),
  ],
});

// The SDK automatically:
// 1. Checks if Permit2 is deployed on the source chain
// 2. Approves the Permit2 contract if needed (one-time)
// 3. Signs a PermitTransferFrom message per transaction
// 4. Encodes the calldata for Permit2Proxy
await executeRoute({ route });
To disable Permit2 and force classic approve() transactions:
await executeRoute({
  route,
  executionOptions: {
    disableMessageSigning: true,
  },
});

When Permit2 Is Not Used

The SDK skips Permit2 and falls back to classic approve() when:
  • The source chain does not have Permit2 deployed (chain.permit2 is not set)
  • The source chain does not have a Permit2Proxy (chain.permit2Proxy is not set)
  • The source token is the chain’s native token (ETH, MATIC, etc.)
  • Message signing is disabled (disableMessageSigning: true)
  • The transaction uses batched execution (EIP-5792)
  • The step’s estimate has skipApproval: true or skipPermit: true (rare, optional fields only present on certain chain-specific steps such as Hyperliquid)

Reference

Permit2Proxy Contract Functions

FunctionDescription
callDiamondWithPermit2(diamondCalldata, permit, signature)Transfers tokens via Permit2 permitTransferFrom, approves the Diamond, and forwards calldata.
callDiamondWithEIP2612Signature(token, amount, deadline, v, r, s, diamondCalldata)Calls permit() on the EIP-2612 token, transfers tokens, approves the Diamond, and forwards calldata.
nextNonce(owner)Returns the next available Permit2 nonce for the given address.

EIP-712 Type Definitions

PermitTransferFrom (Permit2 standard flow):
{
  "TokenPermissions": [
    { "name": "token", "type": "address" },
    { "name": "amount", "type": "uint256" }
  ],
  "PermitTransferFrom": [
    { "name": "permitted", "type": "TokenPermissions" },
    { "name": "spender", "type": "address" },
    { "name": "nonce", "type": "uint256" },
    { "name": "deadline", "type": "uint256" }
  ]
}
Permit (EIP-2612 native permit):
{
  "Permit": [
    { "name": "owner", "type": "address" },
    { "name": "spender", "type": "address" },
    { "name": "value", "type": "uint256" },
    { "name": "nonce", "type": "uint256" },
    { "name": "deadline", "type": "uint256" }
  ]
}

EIP-712 Domain

Permit2 (for PermitTransferFrom):
{
  "name": "Permit2",
  "chainId": "<source chain ID>",
  "verifyingContract": "<Permit2 contract address>"
}
EIP-2612 (for native Permit — domain varies per token):
{
  "name": "<token name>",
  "version": "<token version>",
  "chainId": "<source chain ID>",
  "verifyingContract": "<token contract address>"
}