Permit 代币签名批准

admin
admin 1月4日
  • 在其它设备中阅读本文章

在以太坊生态系统中,ERC-20 代币是最常见的代币标准之一。它定义了代币的基本接口,包括转账、余额查询等功能。然而,随着 DeFi(去中心化金融)的快速发展,传统的 ERC-20 代币在某些场景下显得不够灵活。特别是在需要用户多次授权合约操作时,用户需要多次签名并支付 Gas 费用,这不仅增加了用户的操作成本,还降低了用户体验。

为了解决这个问题,EIP-2612 提出了 permit 功能,允许用户通过链下签名的方式授权合约操作,从而减少链上交易次数和 Gas 消耗。本文将详细介绍 permit 功能的实现原理、使用场景以及如何在智能合约中集成这一功能。

一、什么是 Permit

permit 是 ERC-20 代币的一个扩展功能,它允许用户通过链下签名的方式授权第三方合约或地址使用其代币。与传统的 approve 方法不同,permit 不需要用户发送链上交易,而是通过签名的方式在链下完成授权。合约可以通过验证签名来获取用户的授权,从而减少用户的 Gas 消耗。

1.1 传统 approve 的问题

在传统的 ERC-20 代币中,用户需要通过 approve 方法来授权第三方合约或地址使用其代币。例如:

function approve(address spender, uint256 amount) external returns (bool);

这个过程需要用户发送一笔链上交易,并支付 Gas 费用。如果用户需要多次授权不同的合约或地址,每次都需要发送一笔交易,这不仅增加了用户的操作成本,还降低了用户体验。

1.2 permit 的优势

permit 功能通过链下签名的方式解决了这个问题。用户只需在链下签名一次,合约可以通过验证签名来获取用户的授权。这样,用户不需要发送链上交易,从而减少了 Gas 消耗。

二、Permit 的实现原理

permit 功能的实现依赖于 EIP-712 标准,该标准定义了一种结构化的签名方式,使得链下签名可以被链上合约验证。

2.1 EIP-712 结构化签名

EIP-712 定义了一种结构化的签名方式,允许用户在签名时包含更多的上下文信息。这样,用户在签名时可以清楚地知道自己在签署什么内容,从而减少签名的风险。permit 功能使用 EIP-712 标准来生成和验证签名。具体来说,permit 的签名数据结构如下:

struct Permit {
    address owner;
    address spender;
    uint256 value;
    uint256 nonce;
    uint256 deadline;
}
  • owner: 代币的所有者地址。
  • spender: 被授权的地址。
  • value: 授权的代币数量。
  • nonce: 所有者的当前 nonce 值,用于防止重放攻击。
  • deadline: 签名的过期时间。

2.2 Permit 的签名过程

  • 生成签名数据:用户根据 Permit 结构生成签名数据,并使用 EIP-712 标准进行结构化签名。
  • 发送签名:用户将签名发送给第三方合约。
  • 验证签名:合约通过验证签名来获取用户的授权,并更新用户的 allowance。

2.3 Permit 的合约实现

在智能合约中,permit 功能通常通过以下方式实现:

function permit(
    address owner,
    address spender,
    uint256 value,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    require(block.timestamp <= deadline, "Permit: expired");

    bytes32 structHash = keccak256(
        abi.encode(
            PERMIT_TYPEHASH,
            owner,
            spender,
            value,
            nonces[owner]++,
            deadline
        )
    );

    bytes32 hash = _hashTypedDataV4(structHash);
    address signer = ECDSA.recover(hash, v, r, s);
    require(signer == owner, "Permit: invalid signature");

    _approve(owner, spender, value);
}
  • PERMIT_TYPEHASH: 是 Permit 结构的类型哈希,用于 EIP-712 签名。
  • nonces[owner]: 是用户的当前 nonce 值,用于防止重放攻击。
  • _hashTypedDataV4: 是 EIP-712 的哈希函数,用于生成结构化哈希。
  • ECDSA.recover: 用于从签名中恢复签名者地址。

三、使用 Permit

3.1 permit 功能在以下场景中非常有用

  • 减少 Gas 消耗:在 DeFi 应用中,用户通常需要多次授权合约操作。使用 permit 功能,用户只需在链下签名一次,合约可以通过验证签名来获取用户的授权,从而减少用户的 Gas 消耗。
  • 提高用户体验:传统的 approve 方法需要用户发送链上交易,而 permit 功能允许用户在链下签名,从而提高了用户体验。用户可以在不发送链上交易的情况下完成授权操作。
  • 防止重放攻击:permit 功能使用 nonce 和 deadline 来防止重放攻击。每次签名都有一个唯一的 nonce 值,并且签名有一个过期时间,从而确保签名的安全性。

3.2 在智能合约中集成 Permit

要在智能合约中集成 permit 功能,可以按照以下步骤进行:

继承 ERC-20Permit 合约
OpenZeppelin 提供了一个 ERC20Permit 合约,可以直接继承并使用。ERC20Permit 合约已经实现了 permit 功能,只需继承该合约即可。

import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";

contract MyToken is ERC20Permit {
    constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}
}

使用 Permit 接口
在合约中使用 permit 功能时,可以通过以下方式调用:

function depositWithPermit(
    uint256 amount,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
) external {
    token.permit(msg.sender, address(this), amount, deadline, v, r, s);
    token.transferFrom(msg.sender, address(this), amount);
}

3.3 前端用户生成 Permit 签名

前端用户可以使用 ethers 库快速生成签名

        const token = await ethers.getContractAt(tokenAbi, tokenAddr, signer);
        const nonce = await token.nonces(signer);
        const name = await token.name();
        const version = await token.version();
        const amount = ethers.MaxUint256;
        const deadline = Math.floor(Date.now()/1000) + 3600;
        const domain = {
          name,
          version,
          chainId: chainId,
          verifyingContract: token.target,
        };
        const types = {
          Permit: [
            { name: "owner", type: "address" },
            { name: "spender", type: "address" },
            { name: "value", type: "uint256" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint256" },
          ],
        };
        const value = {
          owner: signer.address,
          spender: router.target,
          value: amount,
          nonce,
          deadline,
        };
        const signature = ethers.Signature.from(await signer.signTypedData(domain, types, value));
        // signature.v,
        // signature.r,
        // signature.s,

四、进阶版 Permit2

Permit2 是 Uniswap 团队提出的一种新型代币授权标准,旨在解决传统 approve 和 EIP-2612 permit 的局限性。它通过引入一个全局的、可重用的授权机制,允许用户在一次链下签名中授权多个合约或操作,从而进一步减少 Gas 消耗并提升用户体验。

Permit2 的名称反映了与 Permit2 合约交互以实现授权转移的两种方式。尽管授权和基于签名的转移之间存在区别,但这两种交互类型都使用签名:

  • 基于授权的转移:通过签名处理代币授权,转移检查允许的金额。这是在预期多次转移时更高效的解决方案。‍
  • 基于签名的转移:直接通过签名处理代币转移。对于一次性转移更高效。

4.1 传统 approve 和 permit 的局限性

  • 多次授权问题:在传统 approve 和 permit 中,用户需要为每个合约或操作单独授权。例如,如果用户想在多个 DeFi 平台上使用代币,每次都需要发送一笔授权交易。
  • Gas 消耗高:每次授权都需要支付 Gas 费用,尤其是在以太坊网络拥堵时,Gas 费用会显著增加。
  • 安全性问题:传统的 approve 方法存在潜在的安全风险,例如授权额度过高可能导致资金被盗。

4.2 Permit2 的优势

  • 全局授权:Permit2 允许用户在一次链下签名中授权多个合约或操作,从而减少链上交易次数。
  • Gas 优化:通过集中管理授权,Permit2 显著降低了 Gas 消耗。
  • 安全性增强:Permit2 引入了更灵活的授权机制,例如时间限制和额度限制,从而提高了安全性。

4.3 Permit2 的设计理念

Permit2 的核心设计理念是通过一个全局的、可重用的授权机制,简化代币授权流程并提高效率。具体来说,Permit2 实现了以下功能:

  • 集中管理授权,Permit2 引入了一个全局的授权管理器(Authorization Manager),用户可以通过一次链下签名授权多个合约或操作。授权管理器会记录用户的授权信息,并在需要时提供给合约使用。
  • 灵活的授权策略,Permit2 支持多种授权策略,例如:

    • 时间限制:用户可以设置授权的有效期,过期后授权自动失效。
    • 额度限制:用户可以设置授权的最大额度,防止合约滥用授权。
    • 操作限制:用户可以限制授权的具体操作,例如只能用于转账或只能用于批准。

4.4 兼容性

Permit2 完全兼容现有的 ERC-20 代币标准,并且可以与 EIP-2612 permit 功能共存。这意味着开发者可以逐步迁移到 Permit2,而无需修改现有的代币合约。
Permit2 也有一些独特的缺点。首先,先决步骤迫使用户批准其代币到 Permit2 合约,这对用户体验和采用造成了静态摩擦。其次,攻击面较窄,直接指向 Permit2 合约。好消息是这些合约简洁、编写良好、经过测试和审计。

4.5 工作流程

  • 用户的先决步骤。

    • 他们必须对其代币进行传统的批准,以便将其发送到 Permit2 合约。
    • 通常以 uint256 最大值进行,只需用户为其代币执行一次。
    • 一旦完成,任何与 Permit2 集成的 dApp 只需请求用户的链下签名即可利用已授予的权限。
  • Permit2 获得用户代币批准后的操作。

    • 用户通过链下签名表达其允许特定 dApp 支出者合约移动其代币的意图。
    • 支出者充当快递员,将意图传递给 Permit2 合约,可以视为支出者与用户之间的守门人或中介。
    • 如果 Permit2 合约验证签名并明确要求正确的数据,则将使用预先批准的授权代表用户将代币转移给支出者。
    • 一旦支出者收到代币,它可以执行用户请求的必要操作。

4.6 Permit2 签名生成

        const amount = ethers.parseUnits("10", 6);
        const deadline = Math.floor(Date.now()/1000) + 3600;
        const nonce = 0;

        const domain = {
          name: "Permit2",
          chainId: chainId,
          verifyingContract: permit2.target,
        };

        const types = {
          PermitBatchTransferFrom: [
            { name: "permitted", type: "TokenPermissions[]" },
            { name: "spender", type: "address" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint256" },
          ],
          TokenPermissions: [
            { name: "token", type: "address" },
            { name: "amount", type: "uint256" },
          ],
        };

        const value = {
          permitted: [
            {
              token: usdt.target,
              amount: amount,
            },
          ],
          spender: router.target,
          nonce: nonce,
          deadline: deadline,
        };
        const signature = await signer.signTypedData(domain, types, value);

五、总结

permit 功能通过链下签名的方式,允许用户在不发送链上交易的情况下授权第三方合约或地址使用其代币。这不仅减少了用户的 Gas 消耗,还提高了用户体验。随着 DeFi 的快速发展,permit 功能将在更多的应用场景中得到广泛应用。