Richown头像
关注
ERC-20 授权机制演进:从 approve 竞态到 EIP-2612 permit 免 Gas 签名实战封面图

ERC-20 授权机制演进:从 approve 竞态到 EIP-2612 permit 免 Gas 签名实战

ERC-20 授权机制演进:从 approve 竞态到 EIP-2612 permit 免 Gas 签名实战

封面信息图

在以太坊 DApp 交互的历史中,“先 Approve(授权)、后 TransferFrom(划转)”是最让用户深恶痛绝的“两步走”体验。

用户为了在 DEX 里换一次代币,必须先发一笔交易授权 Router 合约,等链上确认扣了 Gas 之后,才能再发第二笔交易进行真实的兑换。这不仅浪费了翻倍的 Gas 费用,还在历史上引发过著名的 Approve 前置竞态攻击(Allowance Front-running Attack)。

EIP-2612(ERC-20 Permit 扩展) 彻底终结了这一痛点。它允许用户在链下通过标准 EIP-712 签名授权,并由业务合约在单笔交易内原子完成“验签授权 + 划转”,实现真正的“单笔操作、免 Gas 授权”。


一、传统 Approve 的竞态漏洞与 EIP-2612 的演进

1. 经典 Approve 竞态漏洞剖析

假设 Alice 最初授权了 Bob 100 枚代币。随后 Alice 想要将额度降为 50 枚,于是 Alice 发送了一笔 approve(Bob, 50) 的交易。
此时守候在 Mempool 的 Bob 发现该交易后,立即发起一笔高 Gas 的 transferFrom(Alice, 100) 抢跑在前面,随后 Alice 的 approve(Bob, 50) 生效,Bob 再次调用 transferFrom(Alice, 50)。Bob 最终总共转走了 150 枚代币!

2. EIP-2612 的优雅解法

引入基于链下签名的 permit 函数:

  • 包含 owner, spender, value, nonce, deadline, 以及 ECDSA 签名 (v, r, s);
  • 每次 Permit 强制消耗并递增 nonce,从数学上杜绝了签名重放与竞态漏洞。
sequenceDiagram
    autonumber
    actor User as 用户 (浏览器 MetaMask)
    participant Vault as 业务合约 (Vault.sol)
    participant Token as ERC20 Permit 代币 (USDC / DAI)

    User->>User: 1. 在本地签署 EIP-712 Permit 消息 (0 Gas!)
    User->>Vault: 2. 调用 depositWithPermit(amount, deadline, v, r, s)
    Vault->>Token: 3. permit(User, Vault, amount, deadline, v, r, s)
    Token->>Token: 4. 验证签名并设置 allowance[User][Vault] = amount
    Vault->>Token: 5. transferFrom(User, Vault, amount)
    Token-->>Vault: 6. 代币到账,更新金库状态

二、智能合约端集成 Permit 的完整实现

在业务金库中,我们直接调用代币的 permit 接口,将两笔交易合二为一:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract CyberDepositVault {
    using SafeERC20 for IERC20;

    IERC20 public immutable token;
    IERC20Permit public immutable permitToken;
    mapping(address => uint256) public userBalances;

    event Deposited(address indexed user, uint256 amount);

    constructor(address _tokenAddress) {
        token = IERC20(_tokenAddress);
        permitToken = IERC20Permit(_tokenAddress);
    }

    // 原子一步完成:Permit 验签授权 + 代币充值入库
    function depositWithPermit(
        uint256 amount,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        // 1. 调用代币合约的 permit 设置授权额度
        permitToken.permit(msg.sender, address(this), amount, deadline, v, r, s);

        // 2. 立即划转代币
        token.safeTransferFrom(msg.sender, address(this), amount);

        // 3. 记账
        userBalances[msg.sender] += amount;
        emit Deposited(msg.sender, amount);
    }
}

三、前端 Viem 构造 EIP-2612 签名实战

// utils/permitSign.ts
import { createWalletClient, custom, parseUnits, hexToSignature } from 'viem';
import { mainnet } from 'viem/chains';

export async function signERC20Permit(
  userAddress: `0x${string}`,
  tokenAddress: `0x${string}`,
  spenderAddress: `0x${string}`,
  amount: bigint,
  nonce: bigint,
  tokenName: string
) {
  const walletClient = createWalletClient({
    chain: mainnet,
    transport: custom(window.ethereum),
  });

  const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 小时后过期

  // EIP-712 域定义
  const domain = {
    name: tokenName,
    version: '1',
    chainId: 1,
    verifyingContract: tokenAddress,
  };

  // 标准 EIP-2612 签名类型定义
  const types = {
    Permit: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' },
      { name: 'value', type: 'uint256' },
      { name: 'nonce', type: 'uint256' },
      { name: 'deadline', type: 'uint256' },
    ],
  };

  const rawSignature = await walletClient.signTypedData({
    account: userAddress,
    domain,
    types,
    primaryType: 'Permit',
    message: {
      owner: userAddress,
      spender: spenderAddress,
      value: amount,
      nonce,
      deadline,
    },
  });

  // 分割为 v, r, s 结构
  const sig = hexToSignature(rawSignature);

  return {
    deadline,
    v: Number(sig.v),
    r: sig.r,
    s: sig.s,
  };
}

四、生产环境避坑守则

  1. 代币版本的 EIP-712 Domain 差异:
    部分代币(如早期版本的 USDC)的 version 是 "2" 而不是 "1";还有一些代币的 Domain Name 带有空格。在前端发起签名之前,必须通过 token.DOMAIN_SEPARATOR() 或标准读取接口获取准确的 Domain 配置。
  2. 签名有效期(Deadline)防御:
    不要将 deadline 设置为 type(uint256).max,始终设定一个合理的相对过期时间(如 20 分钟~1 小时),防止未成交的离线签名在很久以后被意外打包。

EIP-2612 是现代 DeFi 开发的标配技能。将冗长的两步授权重构为丝滑的单笔签名,是提升 DApp 转化率最立竿见影的工程优化。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_40635035/article/details/164404669

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--