ERC20授权的更优方案 - ERC20Permit 签名授权

ERC20Permit是什么允许用户通过链下离线签名授权,链上直接处理交易。而不像传统的ERC20需要先链上approve,然后再执行交易逻辑,简化交易的流程及拥有gas费代付的能力:https://learnblockchain.cn/shawn_shaw

ERC20Permit 是什么

允许用户通过链下离线签名授权,链上直接处理交易。而不像传统的 ERC20 需要先链上 approve,然后再执行交易逻辑,简化交易的流程及拥有 gas 费代付的能力。

在许多场景下,我们可以认为 ERC20Permit 等同于 EIP2612,因为 ERC20PermitEIP2612 提案的一种拓展 ERC20 代币协议的方案。而 ERC20Permit 又基于我们上一讲提到的 ERC712 结构化签名来实现的。跳转链接

ERC20Permit 的实现原理

  • ERC20 的转账实现 传统的 ERC20 合约转账实现,需要代币拥有者先调用 approve 函数,将代币的所有权转移给另外一个地址。然后由另外一个地址调用实际的 transferFrom 函数来进行代币的转移。在这期间,需要两次合约的调用。
  • ERC20Permit 的转账实现ERC20Permit,通过将第一次的 approve 函数成数字签名的方式,利用数字签名的身份验证、消息完整、不可抵赖,在线下声明代币的拥有权转移给另一个地址。另一个地址在进行转账时,先调用 Permit 函数,再调用 TransferFrom 函数。依然是两步操作,只是说,把这两步都交给别人来进行操作,使得代币的拥有者无需发起调用,无需支付 gas 费用。 实际上,这个另外的地址如果是合约地址,这两步可以合成一步来执行,即在合约中发起两次调用是 messagecall 相当于只收一次 gas 费,起到节省 gas 的效果。

    流程图如下

image.png

ERC20Permit 的官方实现

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);

基于 ERC20Permit 实现一个简单可离线签的合约

  • MyERC20Permit合约
contract MyERC20Permit is ERC20Permit {
    constructor(string memory name,string memory symbol) ERC20(name,symbol) ERC20Permit(name) {
        // 初始给部署者 mint 一些代币,比如 1000 个
        _mint(msg.sender, 1000 * 1e18);
    }
}
  • 测试脚本 MyERC20PermitTest

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 函数完成代币的转账。 image.png
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
shawn_shaw
shawn_shaw
web3潜水员、技术爱好者、web3钱包开发工程师、欢迎交流工作机会。欢迎骚扰:vx:cola_ocean