ERC20Permit库是ERC20的拓展。本库通过permit方法允许调用者携带owner的链下签名来进行token的授权。这样,ERC20 token的owner不再需要自己调用approve方法进行授权,进而实现了owner的EOA账户无eth也可完成授权操作。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
ERC20Permit库是ERC20的拓展。本库通过permit方法允许调用者携带owner的链下签名来进行token的授权。这样,ERC20 token的owner不再需要自己调用approve方法进行授权,进而实现了owner的EOA账户无eth也可完成授权操作。
具体细则参见:https://eips.ethereum.org/EIPS/eip-2612
继承ERC20Permit合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
contract MockERC20Permit is ERC20Permit {
constructor(string memory name, string memory symbol)
ERC20(name, symbol)
ERC20Permit(name){}
}
全部foundry测试合约:
constructor()
:初始化函数,参数为ERC20的name。该过程实际是在调用父合约EIP712的初始化函数,其中version为"1";DOMAIN_SEPARATOR()
:返回EIP712的domain separator。该值用于计算结构化数据的hash。注:EIP712合约详解参见:https://learnblockchain.cn/article/6464
// 使用utils/Counters库,用于nonce的自增
using Counters for Counters.Counter;
// 对于每个owner都独立维护一个nonce值。该值为签名内容中的一部分,每当使用一个签名授权成功后,对应owner在合约内的nonce值便自增1。这样做的目的是为了防止签名被重用
mapping(address => Counters.Counter) private _nonces;
//(基于EIP712)结构化数据Permit的type hash
bytes32 private constant _PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
// 一个slot的预留,目的是为了保证之前版本合约在升级中slot位置的一致性。
bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT;
// 初始化函数
constructor(string memory name) EIP712(name, "1") {}
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
// 调用EIP712._domainSeparatorV4(),返回domain separator
return _domainSeparatorV4();
}
permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
:使用owner的签名给spender进行授权;nonces(address owner)
:返回owner当前待使用的nonce值。 // 参数:
// - owner: 授权人地址;
// - spender: 被授权人地址;
// - value: 授权额度;
// - deadline: 该签名的过期时间
// - v/r/s:来自owner的签名
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
// 做过期检查(即链上时间不超过传入的deadline)
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
// 计算struct hash(结构化数据成员除了nonce以外,都依赖传参。nonce值是从合约内部取)
bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
// 调用EIP712._hashTypedDataV4(),使用上面求出的struct hash计算签名的digest
bytes32 hash = _hashTypedDataV4(structHash);
// 使用digest和签名进行验签
address signer = ECDSA.recover(hash, v, r, s);
// 如果recover出的signer地址不是owner,表示签名中的内容遭到篡改
require(signer == owner, "ERC20Permit: invalid signature");
// 调用ERC20._approve()为spender进行owner的授权
_approve(owner, spender, value);
}
function nonces(address owner) public view virtual override returns (uint256) {
// 返回owner对应nonce的当前值
return _nonces[owner].current();
}
// 返回owner当前的nonce值并在合约内部自增1
function _useNonce(address owner) internal virtual returns (uint256 current) {
// 获取owner的nonce对应的storage引用
Counters.Counter storage nonce = _nonces[owner];
// current为该nonce的当前值
current = nonce.current();
// storage的nonce值自增1
nonce.increment();
}
foundry代码验证:
contract ERC20PermitTest is Test {
using ECDSA for bytes32;
MockERC20Permit private _testing = new MockERC20Permit("test name", "test symbol");
bytes32 private _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
function test_PermitAndNonces() external {
uint privateKey = 1;
address owner = vm.addr(privateKey);
address spender = address(1);
assertEq(_testing.allowance(owner, spender), 0);
assertEq(_testing.nonces(owner), 0);
// approve with permit()
(uint8 v, bytes32 r, bytes32 s) = _getTypedDataSignature(
privateKey,
owner,
spender,
1024,
_testing.nonces(owner),
block.timestamp
);
_testing.permit(owner, spender, 1024, block.timestamp, v, r, s);
assertEq(_testing.allowance(owner, spender), 1024);
assertEq(_testing.nonces(owner), 1);
// revert if expired
vm.expectRevert("ERC20Permit: expired deadline");
_testing.permit(owner, spender, 1024, block.timestamp - 1, v, r, s);
// revert with if the parameters are changed
(v, r, s) = _getTypedDataSignature(
privateKey,
owner,
spender,
1024,
_testing.nonces(owner),
block.timestamp
);
// case 1: spender is changed
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(owner, address(uint160(spender) + 1), 1024, block.timestamp, v, r, s);
// case 2: owner is changed
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(address(uint160(owner) + 1), spender, 1024, block.timestamp, v, r, s);
// case 3: value is changed
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(owner, spender, 1024 + 1, block.timestamp, v, r, s);
// case 4: deadline is changed
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(owner, spender, 1024, block.timestamp + 1, v, r, s);
// case 5: nonce is changed
(v, r, s) = _getTypedDataSignature(
privateKey,
owner,
spender,
1024,
_testing.nonces(owner) - 1,
block.timestamp
);
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(owner, spender, 1024, block.timestamp, v, r, s);
// case 6: not signed by the owner
(v, r, s) = _getTypedDataSignature(
privateKey + 1,
owner,
spender,
1024,
_testing.nonces(owner),
block.timestamp
);
vm.expectRevert("ERC20Permit: invalid signature");
_testing.permit(owner, spender, 1024, block.timestamp, v, r, s);
}
function _getTypedDataSignature(
uint signerPrivateKey,
address owner,
address spender,
uint value,
uint nonce,
uint deadline
) private view returns (uint8, bytes32, bytes32){
bytes32 structHash = keccak256(abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
));
bytes32 digest = _testing.DOMAIN_SEPARATOR().toTypedDataHash(structHash);
return vm.sign(signerPrivateKey, digest);
}
}
ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!