ERC-20 实战(下):签名授权 Permit 与 ERC-2612 模型

关键词:ERC-2612、Permit、Meta-Transaction、EIP-712、nonces、gasless approval、安全授权

📚 作者:Henry 🧱 系列:《ERC 系列标准全景图解》 · 第 4 篇 👨‍💻 受众:Web3 前端工程师 / 区块链开发者 / Web3入门者 👉 系列持续更新中,建议收藏专栏或关注作者

🧠 为什么需要 permit()

在传统的 ERC-20 授权流程中,用户必须先调用 approve(...) 才能授权合约使用自己的 token。这个过程:

  • 需要一次 on-chain 交易(= 支付 gas)
  • 无法由第三方代为触发(因为 approve 必须由 msg.sender 发起)
  • 用户交互路径长,体验差

为此,ERC-2612 引入了 permit(...),一种离线签名 + 合约校验的授权方式,让用户无需上链即可完成授权。


ERC-2612 是什么?属于哪类扩展?

  • 提出者:Uniswap 等项目提出,编号 EIP-2612
  • 类型:ERC-20 的签名授权扩展
  • 基于:EIP-712 Typed Structured Data 签名规范

适用于:

  • 支持 Meta-Transaction(如 Biconomy、Gasless)
  • 授权 + 操作一步到位(如 DEX 中 “Approve + Swap”)

permit(...) 函数定义

function permit(
  address owner,
  address spender,
  uint256 value,
  uint256 deadline,
  uint8 v,
  bytes32 r,
  bytes32 s
) external;

📌 特点说明:

  • v, r, s:为用户离线签名所得参数
  • deadline:授权有效期,避免 replay attack
  • 合约验证签名后,直接设置 allowance

3. permit() 的工作流程

Permit工作流程

┌────────────┐     off-chain signature    ┌──────────────┐
│   User     │ ─────────────────────────▶ │  Frontend UI │
└────────────┘                            └──────────────┘
       │                                        │
       ▼                                        ▼
     交易构造                            提交签名+参数
       │                                        │
       ▼                                        ▼
┌─────────────────────────────────────────────────┐
│          Token Contract (ERC-2612)              │
│ 1. 校验 owner 对消息体签名正确                      │
│ 2. 校验 nonce 未被使用                            │
│ 3. 校验 deadline 未过期                           │
│ 4. 设置 allowance[owner][spender] = value        │
└─────────────────────────────────────────────────┘

4. 签名消息体结构(符合 EIP-712)

Permit(
  address owner,
  address spender,
  uint256 value,
  uint256 nonce,
  uint256 deadline
)

配合 EIP-712 domain separator:

keccak256(abi.encodePacked(
  "\x19\x01",
  DOMAIN_SEPARATOR,
  keccak256(encode(Permit))
))

非常关键的 nonce 管理机制

ERC-2612 中每个签名必须唯一,使用 nonces[owner] 控制:

mapping(address => uint256) public nonces;

每次调用 permit() 后:

nonces[owner]++;

✅ 能有效防止签名被重放


安全边界与校验规则

校验字段 意义
owner != 0 禁止空地址签名
spender != 0 确保授权对象有效
block.timestamp <= deadline 签名未过期
recover(hash, sig) == owner 确认为本人签名
nonces 保证签名唯一性

源码解读(OpenZeppelin ERC20Permit)

OpenZeppelin 的实现位于 ERC20Permit.sol

function permit(...) public override {
  require(block.timestamp <= deadline, "expired");

  bytes32 structHash = keccak256(abi.encode(
    PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline
  ));

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

  _approve(owner, spender, value);
}

模块继承关系:

ERC20 → ERC20Permit → EIP712 → Nonces

前端实践(以 viem 或 ethers 为例)

import { signTypedData } from 'viem/account'

const signature = await signTypedData({
  domain: {
    name: 'DAI Stablecoin',
    version: '1',
    chainId: 1,
    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: user,
    spender,
    value: amount,
    nonce,
    deadline,
  },
})

合约落地建议

场景 推荐方式
创建支持 permit 的代币 继承 ERC20Permit, 并设置 EIP-712 域名
对接 DEX 使用 permit + swap 组合交易减少 approve 步骤
支持 Gasless 搭配 relayer + permit,实现无 gas 授权
检查 token 是否支持 读取合约 ABI 是否存在 permit(...) 方法

✅ 小结

  • permit() 实现了基于签名的离线授权,不依赖 msg.sender
  • 通过 EIP-712 + nonce + deadline 机制保障安全性
  • 非常适用于减少授权交互、支持 Gasless 的场景
  • 已广泛用于 DAI、USDC、Uniswap、1inch 等主流项目中
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论