ERC20Permit是什么允许用户通过链下离线签名授权,链上直接处理交易。而不像传统的ERC20需要先链上approve,然后再执行交易逻辑,简化交易的流程及拥有gas费代付的能力:https://learnblockchain.cn/shawn_shaw
允许用户通过链下离线签名授权,链上直接处理交易。而不像传统的 ERC20
需要先链上 approve
,然后再执行交易逻辑,简化交易的流程及拥有 gas
费代付的能力。
在许多场景下,我们可以认为 ERC20Permit
等同于 EIP2612
,因为 ERC20Permit
是 EIP2612
提案的一种拓展 ERC20
代币协议的方案。而 ERC20Permit
又基于我们上一讲提到的 ERC712
结构化签名来实现的。跳转链接
ERC20
合约转账实现,需要代币拥有者先调用 approve
函数,将代币的所有权转移给另外一个地址。然后由另外一个地址调用实际的 transferFrom
函数来进行代币的转移。在这期间,需要两次合约的调用。ERC20Permit 的转账实现
而 ERC20Permit
,通过将第一次的 approve
函数成数字签名的方式,利用数字签名的身份验证、消息完整、不可抵赖,在线下声明代币的拥有权转移给另一个地址。另一个地址在进行转账时,先调用 Permit
函数,再调用 TransferFrom
函数。依然是两步操作,只是说,把这两步都交给别人来进行操作,使得代币的拥有者无需发起调用,无需支付 gas
费用。
实际上,这个另外的地址如果是合约地址,这两步可以合成一步来执行,即在合约中发起两次调用是 messagecall
相当于只收一次 gas
费,起到节省 gas
的效果。
流程图如下
在 Openzeppelin
中,官方提供了 ERC20Permit
的实现,我们只需要继承即可。
bytes32 private constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
这是一个常量,意味着每个使用了 ERC20Permit
的合约签名时 Types
都必须遵守这个规则(ERC712
结构化签名的格式)。
/**
Permit 函数,将owner 的 value 代币分配给 spender 使用
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
/**
防止重入攻击
*/
function nonces(address owner) external view returns (uint256);
/**
返回 Doamin 的分隔符(DOMAIN 和 DOMAIN 的值的进行 Hash)
*/
function DOMAIN_SEPARATOR() external view returns (bytes32);
contract MyERC20Permit is ERC20Permit {
constructor(string memory name,string memory symbol) ERC20(name,symbol) ERC20Permit(name) {
// 初始给部署者 mint 一些代币,比如 1000 个
_mint(msg.sender, 1000 * 1e18);
}
}
contract MyERC20PermitTest is Test {
MyERC20Permit public token;
address public owner;
uint256 public ownerPrivateKey;
address public spender;
function setUp() public {
// 生成测试账户
ownerPrivateKey = 0xA11CE;
owner = vm.addr(ownerPrivateKey);
spender = address(0xBEEF);
// 部署代币,切换到 owner 的身份
vm.prank(owner);
token = new MyERC20Permit("MyToken", "MTK");
}
function testPermit() public {
uint256 value = 100e18;
uint256 nonce = token.nonces(owner);
uint256 deadline = block.timestamp + 1 days;
// 构造 permit 需要的 digest
bytes32 digest = getPermitDigest(
"MyToken",
"1",
block.chainid,
address(token),
owner,
spender,
value,
nonce,
deadline
);
// owner 签名
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);
// spender 调用 permit
vm.prank(spender);
token.permit(owner, spender, value, deadline, v, r, s);
// 校验 allowance
uint256 allowance = token.allowance(owner, spender);
assertEq(allowance, value);
// spender 再 transferFrom 成功
vm.prank(spender);
token.transferFrom(owner, spender, value);
assertEq(token.balanceOf(spender), value);
}
// 帮助函数:构造 EIP712 digest
function getPermitDigest(
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
address owner_,
address spender_,
uint256 value_,
uint256 nonce_,
uint256 deadline_
) internal pure returns (bytes32) {
bytes32 DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
verifyingContract
)
);
bytes32 structHash = keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner_,
spender_,
value_,
nonce_,
deadline_
)
);
return keccak256(
abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)
);
}
}
messageHash
:1. 构建 DomainHash
2. 构建 StructHash
3. 组合两者再进行一次 Hash
形成 messageHash
。
然后对 messageHash
使用私钥进行离线签名,发送原数据和 signature
到交给别人。
别人使用原数据和 signature
调用合约上的 Permit
函数完成代币授权。然后调用 TransferFrom
函数完成代币的转账。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!