一 为什么提出ERC-2612
对于ERC20我们知道,通过第三方转账的时候,需要用户限进行approve, 然后第三方才可以进行transferFrom。 那么对于approve和 transferFrom,其实就是两笔交易。所以这样的话,存在两个问题:一是麻烦,必须每次都需要EOA用户授权;二是多笔交易会产生更多的Gas费。因此,在这种背景下,提出来了ERC-2162提案。
二 ERC-2162标准提案规范
2.1 什么是ERC-2612
ERC-2612通过permit函数扩展了ERC-20标准,通过这个函数,代币持有者可以通过签名的方式,直接批准第三方使用它的代币,而不需要再单独提交一个approve申请批准的交易。
2.2 数字签名
2.2.1 什么是数字签名
数字签名指的是利用非对称加密技术,验证数字信息或者身份的真实性和可靠性以及完整性。
数字签名涉及3个步骤:
第一: 创建签名
发送者利用私钥对信息进行加密,这个加密后的哈希值就构成了信息的“数字签名”
第二: 验证签名
接收者利用发送者的公钥对签名进行解密,提取出加密前的哈希值;接收者同时对原始信息使用相同的哈希算法重新计算哈希值,若两个哈希值相同,则验证信息的完整性和来源
2.2.2 数字签名的好处
验证真实性: 确认信息确实由声称的发送者签发
确保完整性: 验证信息自签名后未被篡改过
2.3 EIP-2612签名生成验证
第一: 签名在 EIP-2612 中是使用 EIP-712 标准生成的,这是一种结构化的数据签名方法
第二: 签名通常包括以下元素:
address owner: 代币持有者地址
address spender: 授权第三方地址
uint256 value: 授权数量
uint256 deadline: 到期时间
uint256 nonce:这是一个防止重放攻击的计数器,每次授权成功后都会递增
第三: 离线生成的签名信息:
uint8 v,
bytes32 r,
bytes32 s
三 EIP-2162规范
3.1 构造Permit Struct类型哈希
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
Permit是结构体数据,包含owner, spender, value, nonce, deadline数据
然后对这个字符串通过哈希函数keccak256计算得到一个Permit类型哈希
3.2 nonces
Nonce主要是防止重放攻击的,它的底层原理就是只要每调用一次permit函数,用户维护的nonce就会在之前的基础上+1,从而可以有效防止重放攻击。
function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
abstract contract Nonces {
/**
* @dev The nonce used for an `account` is not the expected current nonce.
*/
error InvalidAccountNonce(address account, uint256 currentNonce);
mapping(address account => uint256) private _nonces;
/**
* @dev Returns the next unused nonce for an address.
*/
function nonces(address owner) public view virtual returns (uint256) {
return _nonces[owner];
}
/**
* @dev Consumes a nonce.
*
* Returns the current value and increments nonce.
*/
function _useNonce(address owner) internal virtual returns (uint256) {
// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be
// decremented or reset. This guarantees that the nonce never overflows.
unchecked {
// It is important to do x++ and not ++x here.
return _nonces[owner]++;
}
}
/**
* @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`.
*/
function _useCheckedNonce(address owner, uint256 nonce) internal virtual {
uint256 current = _useNonce(owner);
if (nonce != current) {
revert InvalidAccountNonce(owner, current);
}
}
}
3.3 DOMAIN_SEPARATOR
function DOMAIN_SEPARATOR() external view virtual returns (bytes32) {
return _domainSeparatorV4();
}
这个主要是通过EIP-712获取DOMAIN_SEPARATOR
3.4 permit
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
if (block.timestamp > deadline) {
revert ERC2612ExpiredSignature(deadline);
}
bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, v, r, s);
if (signer != owner) {
revert ERC2612InvalidSigner(signer, owner);
}
_approve(owner, spender, value);
}
首先: 这个函数就是通过Permit类型哈希产生一个Permit的结构体的值哈希
然后: 根据DOMAIN_SEPARATOR和Permit值哈希构建一个完整的签名哈希
再者: 根据用户提交的离线签名信息:(v,r,s) 然后和计算出来的完整哈希,恢复出来签名地址
最后: 判断是不是代币持有者,如果不是抛出ERC2612InvalidSigner错误;如果是则执行approve操作
四 怎么生成离线签名(链下签名)
EIP712 签名必须包含一个 EIP712Domain 部分,它包含了合约的 name,version(一般是1),chainId,和 verifyingContract(验证签名的合约地址)。
"EIP712Domain": [
{ name: "name", type: "string"},
{ name: "version", type: "string"},
{ name: "chainId", type: "uint256"},
{ name: "verifyingContract", type: "address"},
]
这些信息会在用户签名时显示,并确保只有特定链的特定合约才能验证签名。你需要在脚本中传入相应参数。
const domain = {
name: "Permit",
version: "1",
chainId: "1",
verifyingContract: "0xf2381D47203A594245E36C43e151709F0C15fBe2",
};
根据使用场景自定义一个签名的数据类型,他要与合约匹配
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
创建一个 message 变量,传入要被签名的类型化数据。
const message = {
owner: "0x5C38Da6a701c568545d3fcB03FcB875f56be5dC2",
spender: "0x2B38Da6a701c568545d7fcB03FcB875f56be3d24",
value: "100",
deadline: "XXXXXXX",
};