> ## Documentation Index
> Fetch the complete documentation index at: https://docs.li.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Permit 与 Permit2 授权流程

LI.FI 在 EVM 链上支持三种代币授权策略。除了经典的 `approve()` 交易之外，集成方还可以使用 **EIP-2612 Permit** 或 **Uniswap Permit2**，通过链下签名来授权代币转账，从而减少所需的链上交易数量。

<Tip>
  如果你使用 **LI.FI SDK** 或 **Widget**，Permit2 会被自动处理。本指南面向直接基于 API 构建、希望了解或自行实现 permit 流程的集成方。
</Tip>

## 授权策略概览

| 策略                     | 链上交易                        | 工作原理                                                                                                 |
| ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| **经典 `approve()`**     | 1 笔授权交易 + 1 笔交换/桥接交易        | 用户向 LI.FI Diamond 发送一笔 ERC-20 `approve()`，然后提交交换/桥接交易。                                               |
| **EIP-2612 原生 Permit** | 仅 1 笔交换/桥接交易                | 用户签署一条链下 EIP-712 消息。该签名与交换 calldata 一同提交到 `Permit2Proxy`，由后者对代币合约调用 `permit()`。仅适用于实现了 EIP-2612 的代币。 |
| **Uniswap Permit2**    | 1 次一次性授权 + 1 笔交换/桥接交易（仅需签名） | 用户对 Permit2 合约进行一次授权（无限额度）。此后的每笔交易，用户签署一条链下 EIP-712 消息以授权特定转账。适用于任何 ERC-20 代币。                       |

### 为什么使用 Permit2？

在经典授权模型下，每一次与新 dApp 的交互都需要单独的 `approve()` 交易。Permit2 用一次性的、对规范 Permit2 合约的无限额度授权取而代之。此后所有的授权都通过无 gas 的 EIP-712 签名完成，并具有按转账粒度的控制（精确金额、截止时间、nonce）。

## 架构

所有基于 permit 的流程都会经过 **Permit2Proxy** 外围合约，它充当用户与 LI.FI Diamond 之间的中介：

<img src="https://mintcdn.com/lifi/N2nNfjtFXlJg4AHt/images/permit2.png?fit=max&auto=format&n=N2nNfjtFXlJg4AHt&q=85&s=f802fd8b064b093a5b59cc5dd6d5a819" alt="Permit2 Architecture" width="2110" height="1346" data-path="images/permit2.png" />

### 关键地址

| 合约                  | 地址                                           | 说明                                                                                                                     |
| ------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **LI.FI Diamond**   | `0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE` | 大多数 EVM 链。部分网络使用不同的地址 —— 请务必查询 `GET /v1/chains` 或查看[智能合约地址](/introduction/lifi-architecture/smart-contract-addresses)。 |
| **Uniswap Permit2** | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | 大多数 EVM 链。若干网络使用不同的部署（例如 zkSync、Abstract、Lens、Flare、Sophon、XDC）—— 请务必从 `GET /v1/chains` 读取 `permit2`。                  |
| **Permit2Proxy**    | 因链而异                                         | 查询 `GET /v1/chains` —— 每个 chain 对象都包含 `permit2` 和 `permit2Proxy` 字段。                                                   |

### 通过 API 发现地址

```ts theme={"system"}
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 流程：Permit2（标准自执行）

这是最常见的 permit 流程。它适用于在已部署 Permit2 的链上的**任何 ERC-20 代币**。

<Steps>
  <Step title="获取报价">
    像往常一样请求报价。响应中包含 `estimate.approvalAddress`（用于经典 approve 的 Diamond 地址）以及你所需的链元数据。

    ```ts theme={"system"}
    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());
    ```
  </Step>

  <Step title="检查并设置 Permit2 授权额度（一次性）">
    用户需要对 **Permit2 合约**（而非 Diamond）授权一次。仅当用户尚未为该代币授权过 Permit2 时才需要进行此操作。

    从 chains API 解析 Permit2 和 Permit2Proxy 地址 —— 不要将它们硬编码，因为若干网络使用非规范部署。

    ```ts theme={"system"}
    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 });
    }
    ```
  </Step>

  <Step title="读取下一个可用的 nonce">
    Permit2 使用无序 nonce。`Permit2Proxy` 合约提供了一个 `nextNonce()` 辅助函数，用于查找签名者的下一个未使用的 nonce。

    ```ts theme={"system"}
    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],
    });
    ```
  </Step>

  <Step title="构造并签署 PermitTransferFrom 消息">
    为 `PermitTransferFrom` 构造 EIP-712 类型化数据。其中的 `spender` 是 **Permit2Proxy**（而非 Diamond）。

    ```ts theme={"system"}
    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,
    });
    ```
  </Step>

  <Step title="编码并将交易发送到 Permit2Proxy">
    将 Diamond calldata 包裹在一个以 **Permit2Proxy**（而非 Diamond）为目标的 `callDiamondWithPermit2` 调用中。

    ```ts theme={"system"}
    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),
    });
    ```
  </Step>

  <Step title="跟踪转账状态">
    像往常一样使用 `/status` 端点跟踪交易状态。

    ```ts theme={"system"}
    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');
    ```
  </Step>
</Steps>

## API 流程：EIP-2612 原生 Permit

EIP-2612 permit 仅适用于实现了 `permit()` 函数的代币（例如 USDC、AAVE、UNI）。完全不需要事先进行 `approve()` 交易。

<Warning>
  并非所有代币都支持 EIP-2612。DAI 使用一种非标准的 permit 签名，LI.FI 目前不支持。如果代币未实现 EIP-2612，请退回到经典 `approve()` 或 Permit2。
</Warning>

### 检测 EIP-2612 支持

EIP-2612 没有链上注册表或 ERC-165 接口。唯一可靠的方法是探测代币合约是否具备所需的函数。如果 `nonces()` 和 `DOMAIN_SEPARATOR()` 都成功返回，则该代币支持 EIP-2612。

```ts theme={"system"}
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;
  }
}
```

使用 OpenZeppelin v4.9+ 或 v5.x 部署的代币还会暴露 `eip712Domain()`（[EIP-5267](https://eips.ethereum.org/EIPS/eip-5267)），它在一次调用中返回所有域字段。对于较旧的代币，请分别读取 `name()`、`version()` 和 `DOMAIN_SEPARATOR()`，并重新计算 separator 以进行验证。

<Steps>
  <Step title="获取报价并检索 diamond calldata">
    与标准流程相同：请求报价，然后将 `transactionRequest.data` 用作你的 diamond calldata。
  </Step>

  <Step title="读取代币的 permit nonce">
    EIP-2612 代币按 owner 跟踪 nonce。从代币合约读取当前 nonce。

    ```ts theme={"system"}
    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',
      }),
    ]);
    ```
  </Step>

  <Step title="签署 EIP-2612 Permit 消息">
    其中的 `spender` 是 **Permit2Proxy** 地址。

    ```ts theme={"system"}
    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,
      },
    });
    ```
  </Step>

  <Step title="通过 Permit2Proxy 编码并发送">
    ```ts theme={"system"}
    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),
    });
    ```
  </Step>
</Steps>

## SDK 用法

`@lifi/sdk` 在路由执行期间自动处理 Permit2。无需手动构造签名。

```ts theme={"system"}
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 });
```

若要禁用 Permit2 并强制使用经典 `approve()` 交易：

```ts theme={"system"}
await executeRoute({
  route,
  executionOptions: {
    disableMessageSigning: true,
  },
});
```

## 何时不使用 Permit2

SDK 在以下情况下会跳过 Permit2 并退回到经典 `approve()`：

* 源链未部署 Permit2（`chain.permit2` 未设置）
* 源链没有 Permit2Proxy（`chain.permit2Proxy` 未设置）
* 源代币是该链的原生代币（ETH、MATIC 等）
* 消息签名被禁用（`disableMessageSigning: true`）
* 交易使用批量执行（EIP-5792）
* 步骤的 estimate 带有 `skipApproval: true` 或 `skipPermit: true`（少见的可选字段，仅出现在某些因链而异的步骤上，例如 Hyperliquid）

## 参考

### Permit2Proxy 合约函数

| 函数                                                                                   | 描述                                                            |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `callDiamondWithPermit2(diamondCalldata, permit, signature)`                         | 通过 Permit2 `permitTransferFrom` 转移代币，授权 Diamond，并转发 calldata。 |
| `callDiamondWithEIP2612Signature(token, amount, deadline, v, r, s, diamondCalldata)` | 对 EIP-2612 代币调用 `permit()`，转移代币，授权 Diamond，并转发 calldata。      |
| `nextNonce(owner)`                                                                   | 返回给定地址的下一个可用 Permit2 nonce。                                   |

### EIP-712 类型定义

**PermitTransferFrom**（Permit2 标准流程）：

```json theme={"system"}
{
  "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 原生 permit）：

```json theme={"system"}
{
  "Permit": [
    { "name": "owner", "type": "address" },
    { "name": "spender", "type": "address" },
    { "name": "value", "type": "uint256" },
    { "name": "nonce", "type": "uint256" },
    { "name": "deadline", "type": "uint256" }
  ]
}
```

### EIP-712 域

**Permit2**（用于 `PermitTransferFrom`）：

```json theme={"system"}
{
  "name": "Permit2",
  "chainId": "<source chain ID>",
  "verifyingContract": "<Permit2 contract address>"
}
```

**EIP-2612**（用于原生 `Permit` —— 域因代币而异）：

```json theme={"system"}
{
  "name": "<token name>",
  "version": "<token version>",
  "chainId": "<source chain ID>",
  "verifyingContract": "<token contract address>"
}
```
