摘要
EIP-2612(又称 ERC-2612 / Permit extension for EIP-20)在 EIP-20 基础上新增 permit 接口,允许通过 EIP-712 typed signatures(secp256k1)来更改 allowance,从而无需由 msg.sender 发起 approve 交易
该扩展目的是最小化对现有 ERC-20 生态的入侵,同时解决用户必须持有 ETH 并且需发两笔交易(approve + action)的体验问题
动机
- 传统 ERC-20 的
approve以msg.sender为准,要求用户必须用 EOA 发起第一次交互(或使用合约钱包)。 - 当用户要与智能合约交互(例如 deposit、swap 等),往往需要先
approve,再由合约调用transferFrom:导致至少两笔链上交易、两次 gas 支付。 - 目标是引入最小、通用且兼容的解决方案,使用户能通过离线签名完成授权,从而可在不持有 ETH 的情况下参与某些操作或把批准与主动作合并成单笔链上交易(例如
depositWithPermit)
注意:合约钱包(contract wallets)也能解决部分问题,但在生态中采纳度较低且需要额外的 UI 适配;permit 方案能在不改动现有 UI 的前提下带来多数好处
规范
合规的实现需要在 ERC-20 基础上实现三项额外函数:
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
function nonces(address owner) external view returns (uint);
function DOMAIN_SEPARATOR() external view returns (bytes32);
permit 的成功条件:
- 当前区块时间
block.timestamp必须 ≤deadline owner≠address(0)nonces[owner](permit 调用前)必须等于消息里包含的nonce。v, r, s必须构成来自owner的有效 secp256k1 签名(签名的数据按 EIP-712 的DOMAIN_SEPARATOR与Permit类型哈希构造)
如果以上任一不满足,permit 必须 revert。成功时应执行:
allowance[owner][spender] = valuenonces[owner] += 1- emit
Approval(owner, spender, value)
EIP-712 / ERC-2612 Permit 消息哈希计算笔记:
EIP712 的最终消息哈希(digest)按规范组合为:固定前缀 0x1901 + DOMAIN_SEPARATOR + hashStruct(message),再对整个字节串做 keccak256。Solidity 写法常见为:
bytes32 digest = keccak256(
abi.encodePacked(
hex"1901",
DOMAIN_SEPARATOR,
keccak256(
abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
owner,
spender,
value,
nonce,
deadline
)
)
)
);
要点说明:
hex"1901":EIP-712 固定前缀,防止与普通签名混淆(历史遗留与安全设计)。DOMAIN_SEPARATOR:域分隔符,用于唯一标识签名所属的“域”(通常包含合约名、版本、链 ID、合约地址),防止跨合约/跨链重放攻击。常用计算方式为:
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
address(this)
)
);
对应的 EIP-712 JSON(供 eth_signTypedData 使用)形式如下(可读格式):
{
"types": {
"EIP712Domain": [
{"name":"name","type":"string"},
{"name":"version","type":"string"},
{"name":"chainId","type":"uint256"},
{"name":"verifyingContract","type":"address"}
],
"Permit": [
{"name":"owner","type":"address"},
{"name":"spender","type":"address"},
{"name":"value","type":"uint256"},
{"name":"nonce","type":"uint256"},
{"name":"deadline","type":"uint256"}
]
},
"primaryType": "Permit",
"domain": {
"name": "<erc20name>",
"version": "<version>",
"chainId": <chainid>,
"verifyingContract": "<tokenAddress>"
},
"message": {
"owner": "<owner>",
"spender": "<spender>",
"value": <value>,
"nonce": <nonce>,
"deadline": <deadline>
}
}
原理阐述
permit足够通用,能使任何涉及 ERC-20 的操作用代币本身支付交易(或由 relayer 发起交易),从而简化 UXnonces提供重放保护(每个 owner 都有独立的 nonce)deadline可以限制签名有效期,兼容 relayer 模式:relayer 在拿到签名后可以选择提交或放弃,deadline 是减轻这类可被 withhold 风险的手段- 该 EIP 不强制为所有 ERC-20 操作提供
*_by_signature版本,原因是不同操作对费用、批次等需求不同,并且可以通过permit + helper contracts实现更复杂行为
向后兼容
- 早期已经存在某些类似 permit 的实现(例如
dai的permit,以及其他实现)。这些实现与此规范在细节上有所不同(例如使用bool allowed或expiry字段),导致签名语义不同 - 当前规范与 Uniswap V2 中的实现保持一致(即采用
value、nonce、deadline的 ABI) - 在 EIP 已广泛部署时,规范增加了“当 permit 无效应 revert”的要求,这与已发现的实现一致
安全考量
列举主要安全注意点:
- 前置提交 / front-running:签名提交是可被任何人(或 relayer)在链上执行的;签名者应通过
deadline或其他手段限制风险。前置执行本身对签名者结果并无区别(提交者 = 任何可提交者),但若签名者期望特定方提交则可能被干预。 - ecrecover 在异常时返回 0 地址:实现必须检查
owner != address(0),防止 malformed 签名导致对零地址创建 allowance(zombie funds 风险)。 - ERC-20 原有的 approval race condition(SWC-114)仍然适用:如果使用者改变 allowance 时存在竞态(例如先将 allowance 设为非零再更改),需要相应的注意和 UX 说明。
- 跨链或链分叉重放风险:若
DOMAIN_SEPARATOR在合约部署时硬编码chainId,在链分叉/chain split 情况可能导致跨链 replay;通常建议按 EIP-712 要求构造 domain 或按需重建。 - 签名被 censor / withheld:relayer 可收到签名后不提交,从而阻止签名者使用该签名;deadline 是常用缓解措施。
- 签名不可见变更:签名发生后若 owner 自行提交新的 state(例如更改 nonce),原签名会失效。