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 函数完成代币的转账。

如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!