Michael.W基于Foundry精读Openzeppelin第42期——draft-ERC20Permit.sol

  • Michael.W
  • 更新于 2023-12-15 17:28
  • 阅读 2010

ERC20Permit库是ERC20的拓展。本库通过permit方法允许调用者携带owner的链下签名来进行token的授权。这样,ERC20 token的owner不再需要自己调用approve方法进行授权,进而实现了owner的EOA账户无eth也可完成授权操作。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 draft-ERC20Permit.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/token/ERC20/extensions/draft-ERC20Permit.sol

ERC20Permit库是ERC20的拓展。本库通过permit方法允许调用者携带owner的链下签名来进行token的授权。这样,ERC20 token的owner不再需要自己调用approve方法进行授权,进而实现了owner的EOA账户无eth也可完成授权操作。

具体细则参见:https://eips.ethereum.org/EIPS/eip-2612

1. 目标合约

继承ERC20Permit合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/token/ERC20/extensions/MockERC20Permit.sol

// 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测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/token/ERC20/extensions/ERC20Permit.t.sol

2. 代码精读

2.1 constructor() && DOMAIN_SEPARATOR()

  • 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();
    }

2.2 permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) && nonces(address owner)

  • 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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

公众号名称:后现代泼痞浪漫主义奠基人

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者