EIP-2612:签名授权与业务代扣的边界
EIP-2612 为 ERC-20 增加签名授权:Owner 签署许可,任何提交者都能调用 permit 更新额度。签名不转移代币,也不自动执行 deposit 或 swap;业务合约需要主动组合调用。规范
1. 接口与签名内容
标准追加三个接口:permit(owner, spender, value, deadline, v, r, s)、nonces(owner) 和 DOMAIN_SEPARATOR()。PERMIT_TYPEHASH 是实现常用的常量,标准不要求通过一个公开 getter 暴露它。
签名的类型字符串为:
Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)
摘要计算中的地址和整数必须按照 ABI 编码补齐,不能直接拼接 20 字节地址来替代字段编码:
structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline))
digest = keccak256(0x1901 || DOMAIN_SEPARATOR || structHash)
域和类型化哈希的编码规则见 EIP-712。DAI 风格 permit 的字段与语义不同,不能因为函数名相同就使用这里的签名结构。
2. 成功后改变了什么?
验证要求包括 Owner 非零、签名有效、签入的 nonce 匹配当前 nonce、当前时间不晚于 deadline。成功后覆盖 allowance[owner][spender]、推进 nonce 并发出 Approval;失败时该调用回滚。
deadline 是提交 permit 的截止时间,不是授权额度的有效期。例如在 12:00 前成功授权 100 个单位,12:01 时 Spender 仍可使用剩余额度;期限到达不会自动把 allowance 清零。
标准允许任何人提交已签好的 permit,所以 Relayer 可以抢先消费 nonce。这不能让其修改签名里的 Spender 或金额,但可能使后续重复提交的 permit 回滚。nonce 防止同一许可重复接受,不会自动撤销过去的 allowance。
3. 一个可运行的代币
本例固定为 Solidity 0.8.24、OpenZeppelin Contracts 5.2.0。初始持有人得到 1,000 个、18 位 decimals 的教学代币:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract LearningToken is ERC20, ERC20Burnable, ERC20Permit {
constructor(address initialOwner) ERC20("Learning Token", "LRN") ERC20Permit("Learning Token") {
_mint(initialOwner, 1_000 ether);
}
}
本版本的 ERC20Permit 用 ECDSA 验证,不自动支持 ERC-1271 合约签名。某些代币另有扩展;应按具体代币的源码与部署版本确认,不能从 ERC-1271 标准推导所有 permit 都兼容合约钱包。固定版本源码
4. 为什么可以容忍某些 permit 失败?
若把 permit 与业务代扣放在一笔交易中,授权成功但代扣失败时,本次交易里的 permit 更新也会回滚。这需要业务没有捕获后续转账失败并假装成功;不是 EIP-2612 自动组合两笔交易。
若有人已抢先提交同一许可,重复 permit 会因 nonce 不匹配而失败,但合法 allowance 已存在。OpenZeppelin 文档给出容忍该失败、继续实际代扣的模式。关键是业务身份仍有独立保障,而非只看 permit 有没有成功。集成说明
下面的教学合约绑定 owner = msg.sender、固定代币,并只给同一个调用者记账:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// Teaching model: a fixed standard token, no shares, yield, or withdrawal logic.
contract PermitDeposit is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable token;
mapping(address => uint256) public deposited;
constructor(IERC20 token_) {
token = token_;
}
function deposit(uint256 amount) external nonReentrant {
_deposit(amount);
}
function depositWithPermit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external nonReentrant {
try IERC20Permit(address(token)).permit(msg.sender, address(this), amount, deadline, v, r, s) {} catch {}
_deposit(amount);
}
function _deposit(uint256 amount) private {
token.safeTransferFrom(msg.sender, address(this), amount);
deposited[msg.sender] += amount;
}
}
try/catch 后的 safeTransferFrom 仍必须成功。没有足额 allowance 或余额就回滚,不会凭空增加 deposited。已有额度时,即使当前签名错误或过期,该接口也可以完成普通存款;这是明确选择的集成语义,不代表无效签名被接受。
该模型不适合直接复制成允许任意 Relayer、任意 from 和任意收款人的路由。若需要代提交,应额外验证绑定 Owner、金额、收款人、执行条件和消费状态的业务签名,或采用明确的可信转发上下文。
本例也不处理手续费代币、rebasing 或提款。生产会计若使用不保证到账数量的代币,需要按真实到账量和业务模型设计,不能将 amount 无条件当作收到的资产。
5. Permit2 不是同一种额度模型
用户通常先在原始 ERC-20 上授权 Permit2,之后由 Permit2 验证业务许可。基础 ERC-20 授权仍是单独的权限层。
| Permit2 模块 | 许可的使用方式 | 撤销时要考虑什么 |
|---|---|---|
AllowanceTransfer | 按 Owner、Token、Spender 维护额度、expiration 与 nonce | 当前额度和尚未提交的有效许可分别处理 |
SignatureTransfer | 用一次性签名请求转账,消费无序 nonce | 按 nonce bitmap 使未使用签名失效 |
Witness 用来把额外业务条件纳入签名,不是所有 Permit2 调用必有的“一次性票据”。路由仍必须绑定预期调用者及收款行为,不能把一份给路由的许可当成任意提款权。Permit2 概览、SignatureTransfer
6. 运行测试
完整工程:examples/ethereum-signatures。进入该目录后执行:
npm ci --ignore-scripts
forge test --match-test 'testPermit|testFrontRun|testExpiredPermit|testTransferFailure|testFakePermit|testInvalidPermit' -vv
观察成功许可与代扣只消费一个 nonce;重放失败;抢先提交后 Owner 仍可存款;deadline 后旧额度仍可使用;后续转账失败会回滚许可;无额度不能靠错误签名完成存款。