news 2026/9/15 12:28:43

WTF-Solidity 教程:ERC-2612 ERC20Permit 签名授权实战与源码剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WTF-Solidity 教程:ERC-2612 ERC20Permit 签名授权实战与源码剖析

WTF-Solidity 教程:ERC-2612 ERC20Permit 签名授权实战与源码剖析

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

导读

本文基于 WTF-Solidity 仓库的 ERC20Permit 专题(Languages/pt-br/53_ERC20Permit/readme.md,即 WTF Solidity 极简入门第 53 讲)整理而成。ERC20Permit 是 ERC20 代币标准在 EIP-2612 中提出的扩展,它让授权操作可以脱离msg.sender、改由 EIP-712 链下签名完成,将"授权 + 交易"两步压缩为一步,并被USDCARB等主流代币采用。读完本文,你将掌握 ERC20Permit 的接口定义、合约实现原理(含 nonce 防重放机制与 EIP-712 域分隔符)、基于 ethers v6 的链下签名流程,以及如何用 Remix 完整复现一次"签名即授权"。

ERC20 授权的痛点:为什么需要 Permit?

我们在仓库的 31_ERC20/readme.md 中介绍过 ERC20——以太坊最流行的代币标准。它流行的一个核心原因,是approvetransferFrom两个函数配合使用,使得代币不仅能在外部账户(EOA)之间转移,还能被其他合约(如 DEX)代为划转。

但 ERC20 的approve函数有一个硬性限制:只有代币所有者本人才能调用。这意味着所有 ERC20 代币的初始操作都必须由 EOA 发起。以用户 A 在去中心化交易所用USDT兑换ETH为例,必须依次完成两个交易:

  1. 用户 A 调用approve,将USDT授权给交易所合约;
  2. 用户 A 再次调用合约完成兑换。

这一步两步流程不仅繁琐,还要求用户 A 全程持有ETH来支付两笔交易的 gas。如果用户只有USDT而没有ETH,甚至连第一步授权都无法发起。

ERC20Permit:用 EIP-712 签名替代 approve 交易

EIP-2612 提出的 ERC20Permit 正是为了解决上述痛点:它在 ERC20 标准上增加了一个permit函数,允许用户通过 EIP-712 签名修改授权,而不是依赖msg.sender。这带来两点直接收益:

  1. 授权这步仅需用户在链下签名,减少一笔交易:签名的 gas 成本为零(或由执行方代付);
  2. 签名后用户可委托第三方执行后续交易,自己无需持有 ETH:用户 A 可以把签名发给拥有 gas 的第三方 B,由 B 代为提交交易,完成授权乃至后续兑换。

下图直观对比了两种模式的交易数量差异:传统 ERC20 需要两笔链上交易(Approve + TransferFrom),而 ERC20Permit 只需一笔交易(签名本身在链下完成):

需要注意的是,ERC20Permit 依赖的 EIP-712 签名标准,正是仓库 52_EIP712(WTF Solidity 第 52 讲)讲解的"结构化数据签名":用户在钱包中看到的是可读的Permit类型化数据(owner、spender、value、nonce、deadline),而不是一串难以核验的十六进制哈希,这既提升了安全性也改善了用户体验。

IERC20Permit 接口合约:3 个核心函数

仓库中 ERC20Permit 的接口定义位于 Languages/pt-br/53_ERC20Permit/IERC20Permit.sol,它定义了 3 个函数:

  • permit():根据owner的签名,将owner的 ERC20 代币余额授权给spender,数量为value。接口注释明确了 4 条硬性要求:
    • spender不能是零地址;
    • deadline必须是未来的时间戳(保证签名时效性);
    • vrs必须是owner对 EIP-712 格式函数参数的有效签名(secp256k1 签名三分量);
    • 签名必须使用owner当前的 nonce。
  • nonces():返回owner当前的 nonce。每次为permit()生成签名时都必须包含此值;每次成功调用permit()都会将owner的 nonce 加 1,从而防止同一签名被多次使用(防重放)。
  • DOMAIN_SEPARATOR():返回用于编码permit()签名的域分隔符(domain separator),其定义遵循 EIP-712,用于将签名限定在特定链、特定合约上,防止跨链/跨合约重放。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IERC20Permit { function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; function nonces(address owner) external view returns (uint256); // solhint-disable-next-line func-name-mixedcase function DOMAIN_SEPARATOR() external view returns (bytes32); }

ERC20Permit 合约实现:源码级剖析

仓库中的完整实现位于 Languages/pt-br/53_ERC20Permit/ERC20Permit.sol,它继承自ERC20IERC20Permit与 OpenZeppelin 的EIP712。合约包含2 个状态变量

  • _noncesaddress => uint的映射,记录所有用户当前的 nonce 值;
  • _PERMIT_TYPEHASH:常量,记录permit()函数的类型哈希,其值为keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")——注意字段顺序与接口参数一一对应,链下签名时也必须使用完全相同的类型定义。

合约包含5 个函数

  • 构造函数constructor(string memory name, string memory symbol) EIP712(name, "1") ERC20(name, symbol){}——同时初始化 ERC20 的name/symbol和 EIP-712 的name/version(固定为"1");
  • permit():最核心的函数,执行"检查时效 → 还原消息哈希 → 恢复签名者 → 校验身份 → 授权"五步;
  • nonces():返回_nonces[owner]
  • DOMAIN_SEPARATOR():返回_domainSeparatorV4()
  • _useNonce():内部函数,返回用户当前 nonce 并自增 1("消费 nonce")。

permit() 的完整执行链路

function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public virtual override { // 1. 检查 deadline require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); // 2. 拼接 structHash,并消费 nonce bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); bytes32 hash = _hashTypedDataV4(structHash); // 3. 从签名和消息恢复 signer,并验证签名 address signer = ECDSA.recover(hash, v, r, s); require(signer == owner, "ERC20Permit: invalid signature"); // 4. 执行授权 _approve(owner, spender, value); }

各步骤的底层原理可结合 OpenZeppelin 源码印证:

  • 第 1 步时效检查block.timestamp <= deadline确保签名在有效期内,过期签名直接回滚;
  • 第 2 步消息哈希:先用abi.encode_PERMIT_TYPEHASH与五个参数编码、再取keccak256得到structHash,随后调用_hashTypedDataV4()。该函数定义在 lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol,实现为MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash),即把域分隔符与结构化数据哈希拼接后再做一次keccak256,得到最终待签名摘要。域分隔符由_buildDomainSeparator()生成:keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))),其中TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")。正是这一结构把签名锁定在"特定合约 + 特定链"上;
  • 第 3 步签名恢复ECDSA.recover(hash, v, r, s)定义在 lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol,从(v, r, s)三分量恢复出签名地址,若恢复失败或签名非法会回滚。恢复出的signer必须严格等于owner,否则说明签名的所有者与授权发起者不一致;
  • 第 4 步授权:调用 ERC20 内部的_approve(owner, spender, value),触发标准的Approval事件——至此授权完成,且整个过程不需要owner亲自发起交易。

仓库根目录的官方实现还附带了mint函数(Languages/pt-br/53_ERC20Permit/ERC20Permit.sol末尾的mint(uint amount)),便于在测试环境中铸造代币,方便演练签名授权流程。

与 OpenZeppelin 官方 ERC20Permit 的对照

仓库的lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol提供了 OpenZeppelin 官方实现。对比可见两者核心逻辑完全一致:相同的PERMIT_TYPEHASH类型哈希、相同的"过期检查 → structHash →_hashTypedDataV4ECDSA.recover→ 身份校验 →_approve"链路。差异主要在工程细节上:官方版本使用ERC2612ExpiredSignature/ERC2612InvalidSigner自定义错误而非require字符串(更省 gas),并通过Nonces抽象统一管理 nonce。从源码结构可以看出,本教程的简易实现是为了教学可读性而刻意精简的——理解它的五步流程后,再读官方实现会非常轻松。

Remix 实战复现:从部署到"签名即授权"

教程文档给出了完整的 Remix 复现步骤,我们结合仓库的signERC20Permit.html签名工具逐一展开。

第 1 步:部署 ERC20Permit 合约

在 Remix 中编译并部署 Languages/pt-br/53_ERC20Permit/ERC20Permit.sol,构造函数参数namesymbol均设为WTFPermit。部署完成后,先用合约自带的mint给自己铸造一些代币,保证owner有余额可授权。

第 2 步:链下签名,获取 v / r / s

仓库在 Languages/pt-br/53_ERC20Permit/signERC20Permit.html 提供了一套基于 ethers v6 的浏览器签名工具。操作流程如下:

  1. 浏览器打开该 HTML 文件(需处于支持 MetaMask 注入的环境中,也可直接作为本地静态页面运行);
  2. Contract Address改为刚部署的ERC20Permit合约地址,其余信息按下面给出的参数填写;
  3. 依次点击Connect MetaMask(连接钱包)与Sign Permit(发起签名);
  4. 在 MetaMask 弹出的 EIP-712 签名确认框中核对Permit结构化数据后签名,页面会自动解析出vrs三分量。

文档给出的示例参数如下(注意:owner必须与签名钱包一致,例如 Remix 默认测试钱包):

owner: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 spender: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 value: 100 deadline: 115792089237316195423570985008687907853269984665640564039457584007913129639935 private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb

其中deadline使用了uint256的最大值2^256 - 1,表示"永不过期",是实践中常见的便利写法。

签名页面(MetaMask 的 EIP-712 类型化数据签名请求)效果如下图所示——用户在签名前可以看到Permit的全部字段(owner、spender、value、nonce、deadline),这正是 EIP-712 相对普通personal_sign的核心安全优势:

从 signERC20Permit.html 的源码可以看到签名工具的完整逻辑:它用ethers.BrowserProvider(window.ethereum)获取钱包 Provider,构造domain(含name: "WTFPermit"version: "1"chainIdverifyingContract)、types(与链上_PERMIT_TYPEHASH完全对应的Permit五字段类型)和message(owner/spender/value/nonce/deadline),最后调用signer.signTypedData(domain, types, message)得到签名串,再用ethers.Signature.from(signature)拆出vrs链下的类型定义与链上的_PERMIT_TYPEHASH必须严格一致,否则ECDSA.recover恢复出的地址不会等于ownerpermit()会以 "invalid signature" 回滚。

第 3 步:调用 permit() 完成链上授权

将第 2 步获取的(v, r, s)连同ownerspendervaluedeadline一起作为参数,在 Remix 中调用合约的permit()方法。这笔交易可以由任何地址(包括第三方 B)发起——这正是 ERC20Permit "签名后无需 owner 持有 ETH" 的体现。

第 4 步:验证授权结果

调用合约的allowance()方法,传入对应的ownerspender,即可看到授权额度已变为value(示例中为 100),说明permit()已成功执行_approve。此后,spender就可以通过transferFrom划转这笔额度,完成诸如 DEX 兑换之类的后续操作。

安全注意事项:签名即授权,谨慎再谨慎

ERC20Permit 把"链下签名"与"链上授权"绑定在一起,便利的同时也放大了风险:

  1. 钓鱼攻击风险:黑客会利用这一特性诱导用户签署恶意签名,一旦签名泄露,攻击者可直接调用permit()把受害者资产授权给自己并划走。文档记载了 2023 年 4 月一起针对 USDC 的签名钓鱼攻击,导致一位用户损失 228 万美元。签名时,一定要仔细阅读签名内容,确认spendervaluedeadline等每一项数据都符合预期,切勿盲目点击钱包的 Sign 按钮。

  2. nonce 抢跑导致的 DoS 风险permit()执行时会消耗当前 nonce(_useNonce自增 1)。部分合约在集成permit时,如果其某个函数内部包含 permit 操作(如"先签名授权、再在单笔交易内完成转账"),攻击者可以通过抢跑(front-run)先提交permit()交易,从而占用该 nonce,使目标交易因 nonce 不匹配而回滚,造成拒绝服务。集成方在设计"签名授权 + 交易"原子化流程时需要评估这一攻击面。

总结

本讲介绍了 ERC20Permit(EIP-2612)——一个扩展自 ERC20 标准、支持用户通过 EIP-712 链下签名完成授权的机制。它把传统 ERC20 两步式授权(approve+ 交易)压缩为"一次签名 + 一笔交易",token 持有者无需持有 ETH 即可完成授权,因此被USDCARB等大量项目采用。配套的接口合约、实现合约与浏览器签名工具均可在仓库 Languages/pt-br/53_ERC20Permit 目录中找到;OpenZeppelin 官方实现位于 lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol,可作为生产级参考。同时务必牢记:签名即授权,一个签名就可能卷走你的全部资产,签名前请谨慎核对内容。

【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:26:52

KITTI点云预处理与Complex-YOLO训练数据制作详解

1. 项目背景与整体设计思路做3D点云目标检测&#xff0c;尤其是跑Complex-YOLO这种算得上“老前辈”的方案&#xff0c;第一步往往不是搭网络&#xff0c;而是跟数据死磕到底。这话一点都不夸张&#xff0c;我见过不少新手一上来就急着clone仓库、装依赖&#xff0c;结果模型还…

作者头像 李华
网站建设 2026/9/15 12:26:39

微信小程序商城源码模板拆解:从页面结构到接口对接

简介&#xff1a;简易手机商城微信小程序页面模板源码&#xff0c;适合中小商家、前端初学者或需要快速上线商城业务的开发者&#xff0c;可在微信内搭建具备浏览、选购、结算能力的线上店铺。资源包含163个文件&#xff0c;压缩包约1.02MB&#xff0c;主要类型涵盖PNG界面素材…

作者头像 李华
网站建设 2026/9/15 12:24:17

学术论文AI检测率飙升的应对策略与技术解析

1. 论文AI率飙升的现状与挑战2026年的学术圈正面临一个前所未有的困境——论文AI率普遍高达96%以上。作为一名在学术出版领域工作多年的编辑&#xff0c;我每天经手的稿件中&#xff0c;几乎每篇都能发现明显的AI生成痕迹。从公式推导到文献综述&#xff0c;甚至连实验数据都开…

作者头像 李华