我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们介绍 ERC20 代币的一个拓展,ERC20Permit,支持使用签名进行授权,改善用户体验。它在 EIP-2612 中被提出,已纳入以太坊标准,并被 USDC
,ARB
等代币使用。
ERC20
我们在31讲中介绍了ERC20,以太坊最流行的代币标准。它流行的一个主要原因是 approve
和 transferFrom
两个函数搭配使用,使得代币不仅可以在外部拥有账户(EOA)之间转移,还可以被其他合约使用。
但是,ERC20的 approve
函数限制了只有代币所有者才能调用,这意味着所有 ERC20
代币的初始操作必须由 EOA
执行。举个例子,用户 A 在去中心化交易所使用 USDT
交换 ETH
,必须完成两个交易:第一步用户 A 调用 approve
将 USDT
授权给合约,第二步用户 A 调用合约进行交换。非常麻烦,并且用户必须持有 ETH
用于支付交易的 gas。
ERC20Permit
EIP-2612 提出了 ERC20Permit,扩展了 ERC20 标准,添加了一个 permit
函数,允许用户通过 EIP-712 签名修改授权,而不是通过 msg.sender
。这有两点好处:
- 授权这步仅需用户在链下签名,减少一笔交易。
- 签名后,用户可以委托第三方进行后续交易,不需要持有 ETH:用户 A 可以将签名发送给 拥有gas的第三方 B,委托 B 来执行后续交易。

合约
IERC20Permit 接口合约
首先,让我们学习下 ERC20Permit 的接口合约,它定义了 3 个函数:
-
permit()
: 根据owner
的签名, 将owenr
的ERC20代币余额授权给spender
,数量为value
。要求:spender
不能是零地址。deadline
必须是未来的时间戳。v
,r
和s
必须是owner
对 EIP712 格式的函数参数的有效keccak256
签名。- 签名必须使用
owner
当前的 nonce。
-
nonces()
: 返回owner
的当前 nonce。每次为permit()
函数生成签名时,都必须包括此值。每次成功调用permit()
函数都会将owner
的 nonce 增加 1,防止多次使用同一个签名。 -
DOMAIN_SEPARATOR()
: 返回用于编码permit()
函数的签名的域分隔符(domain separator),如 EIP712 所定义。
ERC20Permit 合约
下面,让我们写一个简单的 ERC20Permit 合约,它实现了 IERC20Permit 定义的所有接口。合约包含 2 个状态变量:
_nonces
:address -> uint
的映射,记录了所有用户当前的 nonce 值,_PERMIT_TYPEHASH
: 常量,记录了permit()
函数的类型哈希。
合约包含 5 个函数:
- 构造函数: 初始化代币的
name
和symbol
。 permit()
: ERC20Permit 最核心的函数,实现了 IERC20Permit 的permit()
。它首先检查签名是否过期,然后用_PERMIT_TYPEHASH
,owner
,spender
,value
,nonce
,deadline
还原签名消息,并验证签名是否有效。如果签名有效,则调用ERC20的_approve()
函数进行授权操作。nonces()
: 实现了 IERC20Permit 的nonces()
函数。DOMAIN_SEPARATOR()
: 实现了 IERC20Permit 的DOMAIN_SEPARATOR()
函数。_useNonce()
: 消费nonce
的函数,返回用户当前的nonce
,并增加 1。
Remix 复现
-
部署
ERC20Permit
合约,将name
和symbol
均设为WTFPermit
。 -
运行
signERC20Permit.html
,将Contract Address
改为部署的ERC20Permit
合约地址,其他信息下面给出。然后依次点击Connect Metamask
和Sign Permit
按钮签名,并获取r
,s
,v
,用于合约验证。签名要使用部署合约的钱包,比如 Remix 测试钱包:

-
调用合约的
permit()
方法,输入相应参数,进行授权。 -
调用合约的
allowance()
方法,输入相应的owner
和spender
,可以看到授权成功。
安全注意
ERC20Permit 利用链下签名进行授权给用户带来了便利,同时带来了风险。一些黑客会利用这一特性进行钓鱼攻击,骗取用户签名并盗取资产。2023年4月的一起针对 USDC 的签名钓鱼攻击让一位用户损失了 228w u 的资产。
签名时,一定要谨慎的阅读签名内容!
同时,一些合约在集成permit
时,也会带来DoS(拒绝服务)的风险。因为permit
在执行时会用掉当前的nonce
值,如果合约的函数中包含permit
操作,则攻击者可以通过抢跑执行permit
从而使得目标交易因为nonce
被占用而回滚。
总结
这一讲,我们介绍了 ERC20Permit,一个 ERC20 代币标准的拓展,支持用户使用链下签名进行授权操作,改善了用户体验,被很多项目采用。但同时,它也带来了更大的风险,一个签名就能将你的资产卷走。大家在签名时一定要更加谨慎。