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

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

在这里插入图片描述

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
毕业设计,基于SpringBoot+Vue+MySQL开发的公寓报修管理系统,源码+数据库+毕业论文+视频演示 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本公寓报修管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理效率,达到事半功倍的效果。此公寓报修管理系统利用当下成熟完善的Spring Boot框架,使用跨平台的可开发大型商业网站的Java语言,以及最受欢迎的RDBMS应用软件之一的MySQL数据库进行程序开发。公寓报修管理系统有管理员,住户,维修人员。管理员可以管理住户信息和维修人员信息,可以审核维修人员的请假信息,住户可以申请维修,可以对维修结果评价,维修人员负责住户提交的维修信息,也可以请假。公寓报修管理系统的开发根据操作人员需要设计的界面简洁美观,在功能模块布局上跟同类型网站保持一致,程序在实现基本要求功能时,也为数据信息面临的安全问题提供了一些实用的解决方案。可以说该程序在帮助管理者高效率地处理工作事务的同时,也实现了数据信息的整体化,规范化与自动化。 关键词:公寓报修管理系统;Spring Boot框架;MySQL;自动化;VUE
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值