什么是 ERC-2612#
ERC-2612: Permit Extension for EIP-20 Signed Approvals
ERC-2612 是针对 erc20 中的 approve 的优化,传统的 approve 必须由 EOA 发起,对于 EOA 来说 approve + 后续操作就是至少两笔交易,有一些额外的 gas 开销。erc2612 是对 erc20 的拓展,引入新的方法来实现 approve
erc-2612 需要实现三个新方法:
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 方法,这个方法同样也会修改 erc20 中的 approval 结构,释放 event
permit 方法需要传入 7 个参数:
- owner:当前 token 的 owner
- spender:授权人
- value:approve 的额度
- deadline:交易执行的截止日期
- v:交易签名数据
- r:交易签名数据
- s:交易签名数据
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
ERC-2612 的使用流程#
permit#
permit 的主要流程包括:
- 校验当前的交易 deadline
- 通过 ecrecover 解析出 recoveredAddress 地址
- 校验 recoveredAddress 是否与传入的 owner 地址一致
- 执行 approve 逻辑,修改 allowance 并释放 Approval 事件
重要的流程就是第二步,根据传入的参数解析出 recoveredAddress,并校验与传入的 owner 一致,再执行类似:token.approve(spender, value)
的逻辑完成 approve
首先通过uncheck
包装内部的逻辑,使用 uncheck 有几个用处:
- 解决 solidity 0.8.0 之后针对溢出默认 revert 与之前代码冲突的问题,比如我能确保自己的这段运算不会溢出,就可以在外面用 uncheck 包装,不让 solidity 来默认帮我做溢出的判断,这会引入额外的 gas 开销
- 在 0.8.0 之后不再需要 safemath
其次又用了一个关键词ecrecover
这个方法的输入参数是:
- bytes32:签名后的消息
- uint8: v
- bytes32: r
- bytes32: s
返回的参数是签名的地址 recoveredAddress
因为 rvs 都是传入的参数,继续看签名的消息是啥
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
)
keccak256
是在 solidity 中用来计算 hash 的内置方法, abi.encodePacked
是对传入的参数进行紧密的编码(省去 0 的填充,节省空间不与合约交互时使用)
DOMAIN_SEPARATOR#
其中还有一个方法:DOMAIN_SEPARATOR()
这也是 erc-2612 中需要实现的方法,这个字段的目的是把每个链上的每个合约都做唯一性标识,并满足 EIP-712 的要求(一个标准的结构化签名协议,保证链下签名,链上校验的安全性,其中的 domain separator 是一个唯一标识符防止重放攻击)
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
这个示例的DOMAIN_SEPARATOR()
方法使用 keccak256 对合约名称,chainID,版本号,当前合约地址等信息进行哈希计算得出的唯一标识
结束计算DOMAIN_SEPARATOR
后,又通过 abi.encode 对这一段进行编码:
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
这里的参数都是 permit 传入的,需要注意的是nonce
这个字段,使用的是nonces[owner]++
,通过自定义的数组来维护自增的用户 nonce,避免用户同一笔签名的交易被重复利用
由此流程,便从哈希后的签名信息中解析出了 recoveredAddress 地址,那么为什么解析出的 recoveredAddress 地址就和 owner 一定一致呢?
如何使用 ERC-2612#
前面提到,通过调用 permit 方法即可完成 approve 的代理调用,省去 EOA 前置 approve 的操作,在调用 permit 时有非常重要的三个参数 r s v,那么这三个参数是需要 EOA 前置签名好提供过来的
通过 forge 的下面一段代码为例:
address alice = vm.addr(1);
bytes32 hash = keccak256("Signed by Alice");
(uint8 v, bytes32 r, bytes32 s) = vm.sign(1, hash);
address signer = ecrecover(hash, v, r, s);
assertEq(alice, signer); // [PASS]
alice 对 hash 进行签名,即返回 vrs 三个参数,再通过 ecrecover 即得到 signer 即为 alice
那么为了通过 permit 中的校验逻辑,我们也需要构造一致的签名内容让 EOA 完成签名,我们再拿着 rsv 去调用 permit 即可
下面一段代码就是对 permit 签名方法的封装,定义了 Permit 结构,与 erc-2612 中的计算方式一致
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract SigUtils {
bytes32 internal DOMAIN_SEPARATOR;
constructor(bytes32 _DOMAIN_SEPARATOR) {
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
}
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH =
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}
// computes the hash of a permit
function getStructHash(Permit memory _permit)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
PERMIT_TYPEHASH,
_permit.owner,
_permit.spender,
_permit.value,
_permit.nonce,
_permit.deadline
)
);
}
// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
function getTypedDataHash(Permit memory _permit)
public
view
returns (bytes32)
{
return
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
getStructHash(_permit)
)
);
}
}
ERC-2612 安全问题#
ERC-2612 本质上就是在 approve 之上添加了一组校验逻辑,允许用户在链下提前生成交易的签名,再将交易签名的 rsv 作为参数传入调用 permit
我可以想到可能存在安全漏洞:
重放攻击:在计算DOMAIN_SEPARATOR
时会使用 chainID 作为标识符,如果链发生分叉,且 chainID 是在构造函数时即设定,那么在两条链上会有同一个 chainID 的合约,可以在链 A 上签名,去链 B 上重放