什麼是 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 上重放