Michael.W基于Foundry精读Openzeppelin第39期——ERC20.sol

  • Michael.W
  • 更新于 2023-12-04 17:42
  • 阅读 715

Openzeppelin中的ERC20库只提供了mint接口,而具体的发行逻辑需要开发者在其子合约中使用_mint()自行编写。该库同样遵循了OpenZeppelin的合约设计思路:当函数因产生错误返回false时,直接revert掉。这种设计思路与ERC20的期望标准并不冲突。

0. 版本

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

0.1 ERC20.sol

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

Openzeppelin中的ERC20库只提供了mint接口,而具体的发行逻辑需要开发者在其子合约中使用_mint()自行编写。该库同样遵循了OpenZeppelin的合约设计思路:当函数因产生错误返回false时,直接revert掉。这种设计思路与ERC20的期望标准并不冲突。

此外,调用transferFrom()时会抛出Approval事件,应用可以通过监听该event来重建所有账户的授权额度。其他EIP的实现合约可能不会抛出此类事件,因为规范没有明确要求。

该ERC20库中的decreaseAllowance()increaseAllowance()方法非ERC20标准方法。

1. 目标合约

继承ERC20合约并暴露_mint()_burn()方法:

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

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract MockERC20 is ERC20 {
    constructor(string memory name, string memory symbol)
    ERC20(name, symbol) {}

    function mint(address account, uint amount) external {
        _mint(account, amount);
    }

    function burn(address account, uint amount) external {
        _burn(account, amount);
    }
}

全部foundry测试合约:

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

2. 代码精读

2.1 一些getter函数

    // 各账户余额的mapping
    // key: 账户地址
    // value: 账户余额
    mapping(address => uint256) private _balances;

    // 各账户授权额度的mapping
    // key1: 授权人地址
    // key2: 操作人地址(被授权的人地址)
    // value: 授权额度
    mapping(address => mapping(address => uint256)) private _allowances;

    // token的发行总量
    uint256 private _totalSupply;

    // ERC20 token的名字
    string private _name;
    // ERC20 token的符号,通常是name的缩写
    string private _symbol;

    // 初始化函数做了name和symbol的设置
    // 该库的默认精度(decimals)是18,如果需要使用别的精度请手动修改decimals()的返回值
    constructor(string memory name_, string memory symbol_) {
        // 只允许在合约部署时设置name和symbol,后续不允许修改
        _name = name_;
        _symbol = symbol_;
    }

    // IERC20Metadata中的方法,返回ERC20的name
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    // IERC20Metadata中的方法,返回ERC20的symbol
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    // IERC20Metadata中的方法,返回ERC20的精度。如果精度是3且合约中的余额值为12345,那么其在业务逻辑中表示的余额是12345/(10**3)。
    // 这种精度的设计是为了使用整数来表示浮点数
    function decimals() public view virtual override returns (uint8) {
        return 18;
  }  

    // IERC20中的方法,返回ERC20的总发行量
    function totalSupply() public view virtual override returns (uint256) {
        return _totalSupply;
    }

    // IERC20中的方法,返回account的余额
    function balanceOf(address account) public view virtual override returns (uint256) {
        return _balances[account];
    }

    // IERC20中的方法,返回owner授权给spender的额度
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

foundry代码验证:

contract ERC20Test is Test {
    MockERC20 private _testing = new MockERC20("test name", "test symbol");

    function test_Getter() external {
        assertEq(_testing.name(), "test name");
        assertEq(_testing.symbol(), "test symbol");
        assertEq(_testing.decimals(), 18);
        assertEq(_testing.totalSupply(), 0);
    }
}

2.2 _mint(address account, uint256 amount) && _burn(address account, uint256 amount)

  • _mint(address account, uint256 amount):为account增发数量为amount的ERC20 token,会同步增加total supply的数量。注: 1. acount地址不可为0地址; 2. 该铸币操作同样会抛出Transfer事件,其中的from地址为0地址;
  • _burn(address account, uint256 amount):销毁account名下数量为amount的ERC20 token,会同步减少total supply的数量。注:1. account不可为0地址;2. account名下ERC20 token余额要>=amount; 3. 该销毁操作同样会抛出Transfer事件,其中的to地址为0地址。
    function _mint(address account, uint256 amount) internal virtual {
        // 检查account地址不为0地址
        require(account != address(0), "ERC20: mint to the zero address");

        // 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
        _beforeTokenTransfer(address(0), account, amount);
    // 增加ERC20 token的发行总量,改变量为amount
        _totalSupply += amount;
        // 关闭solidity 0.8版本的安全数学检查
        unchecked {
            // 增加account的余额,改变量为amount
            // 注:此处加法不会发生溢出,这是因为在前面total supply的自增已经做了溢出检查。如果total supply自增amount不溢出,那么account余额自增amount一定不会溢出
            _balances[account] += amount;
        }
        // 抛出Transfer事件,其中from为0地址
        emit Transfer(address(0), account, amount);
    // 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
        _afterTokenTransfer(address(0), account, amount);
    }

    function _burn(address account, uint256 amount) internal virtual {
        // 检查account地址不为0地址
        require(account != address(0), "ERC20: burn from the zero address");

    // 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
        _beforeTokenTransfer(account, address(0), amount);

    // 获取account的余额
        uint256 accountBalance = _balances[account];
        // 要求account的余额>=amount
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        // 关闭solidity 0.8版本的安全数学检查
        unchecked {
            // 减少account的余额,改变量为amount
            _balances[account] = accountBalance - amount;
            // 减少total supply数量,改变量为amount。此处不会发生减法溢出,因为前面已经做了accountBalance >= amount的校验,自然totalSupply>=accountBalance>=amount
            _totalSupply -= amount;
        }
    // 抛出Transfer事件,其中to为0地址
        emit Transfer(account, address(0), amount);
    // 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
        _afterTokenTransfer(account, address(0), amount);
    }

    // hook函数_beforeTokenTransfer(),可在该方法中自定义转账前的操作逻辑。本合约中该hook函数为空函数
    // 传参条件:
    // - 当from和to均非0地址时,表示正常转账;
    // - 当from为0地址时,表示为to铸币;
    // - 当to为0地址时,表示销毁to的token
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

    // hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑。本合约中该hook函数为空函数
    // 传参条件:
    // - 当from和to均非0地址时,表示正常转账;
    // - 当from为0地址时,表示为to铸币;
    // - 当to为0地址时,表示销毁to的token
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

foundry代码验证:

contract ERC20Test is Test {
    MockERC20 private _testing = new MockERC20("test name", "test symbol");
    address private owner = address(1);

    event Transfer(address indexed from, address indexed to, uint value);

    function test_MintAndBurn() external {
        // 1. mint()
        assertEq(_testing.balanceOf(owner), 0);
        assertEq(_testing.totalSupply(), 0);

        vm.expectEmit(true, true, false, true, address(_testing));
        emit Transfer(address(0), owner, 1024);
        _testing.mint(owner, 1024);
        assertEq(_testing.balanceOf(owner), 1024);
        assertEq(_testing.totalSupply(), 1024);

        // revert if account is 0 address when mint
        vm.expectRevert("ERC20: mint to the zero address");
        _testing.mint(address(0), 1024);

        // 2. burn()
        vm.expectEmit(true, true, false, true, address(_testing));
        emit Transfer(owner, address(0), 1);
        _testing.burn(owner, 1);
        assertEq(_testing.balanceOf(owner), 1024 - 1);
        assertEq(_testing.totalSupply(), 1024 - 1);

        // revert if amount > balance when burn
        vm.expectRevert("ERC20: burn amount exceeds balance");
        _testing.burn(owner, 1024);

        // revert if account is 0 address when burn
        vm.expectRevert("ERC20: burn from the zero address");
        _testing.burn(address(0), 1);
    }
}

2.3 approve(address spender, uint256 amount) && increaseAllowance(address spender, uint256 addedValue) && decreaseAllowance(address spender, uint256 subtractedValue)

  • approve(address spender, uint256 amount):IERC20中的方法,调用者给spender授权数量为amount的额度。注: 1. 当授权额度amount设置为type(uint256).max时,在spender调用transferFrom()时不会做任何与授权相关的检查和修改。这等同于spender拥有了无限的授权额度; 2. spender不可以为0地址;
  • increaseAllowance(address spender, uint256 addedValue):调用者为spender追加授权addedValue数量的授权额度。该方法不是IERC20中的方法且spender不可以为0地址;
  • decreaseAllowance(address spender, uint256 subtractedValue):调用者为spender减少subtractedValue数量的授权额度。该方法不是IERC20中的方法。注:1. spender不可以为0地址;2. spender来自调用者的额度必须>=subtractedValue。
    function approve(address spender, uint256 amount) public virtual override returns (bool) {
        // 获取调用者地址
        address owner = _msgSender();
        // 调用_approve()方法进行授权的操作
        _approve(owner, spender, amount);
        // 更新授权成功后返回true
        return true;
    }

    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        // 获取调用者地址(即owner)
        address owner = _msgSender();
        // 修改owner给spender的授权额度,更新后的额度为当前额度+addedValue
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        // 更新授权成功后返回true
        return true;
    }

    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        // 获取调用者地址(即owner)
        address owner = _msgSender();
        // 获取当前spender来自owner的授权额度
        uint256 currentAllowance = allowance(owner, spender);
        // 要求当前spender来自owner的授权额度>=subtractedValue
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        // 关闭solidity 0.8版本的安全数学检查
        unchecked {
            // 更新spender来自owner的授权额度,额度为currentAllowance - subtractedValue
            _approve(owner, spender, currentAllowance - subtractedValue);
        }

    // 更新授权成功后返回true
        return true;
    }

    // owner授权给spender数量为amount的额度。可以在该internal函数中根据业务需求增加自动为子系统授权的功能
    // 注:
    // 1. owner地址不能为0地址;
    // 2. spender地址不能为0地址
    function _approve(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        // 检查owner地址不为0地址
        require(owner != address(0), "ERC20: approve from the zero address");
        // 检查spender地址不为0地址
        require(spender != address(0), "ERC20: approve to the zero address");
    // 修改spender的来自owner的授权额度为amount
        _allowances[owner][spender] = amount;
        // 抛出事件
        emit Approval(owner, spender, amount);
    }

foundry代码验证:

contract ERC20Test is Test {
    MockERC20 private _testing = new MockERC20("test name", "test symbol");
    address private owner = address(1);
    address private spender = address(2);

    event Approval(address indexed owner, address indexed spender, uint value);

    function test_ApproveAndIncreaseAllowanceAndDecreaseAllowance() external {
        // 1. approve()
        assertEq(_testing.allowance(owner, spender), 0);
        vm.expectEmit(true, true, false, true, address(_testing));
        emit Approval(owner, spender, 1024);
        vm.prank(owner);
        _testing.approve(spender, 1024);
        assertEq(_testing.allowance(owner, spender), 1024);

        // revert with 0 address as owner or spender
        vm.expectRevert("ERC20: approve from the zero address");
        vm.prank(address(0));
        _testing.approve(spender, 0);
        vm.expectRevert("ERC20: approve to the zero address");
        _testing.approve(address(0), 0);

        // 2. increaseAllowance()
        vm.expectEmit(true, true, false, true, address(_testing));
        emit Approval(owner, spender, 1024 + 1);
        vm.prank(owner);
        _testing.increaseAllowance(spender, 1);
        assertEq(_testing.allowance(owner, spender), 1024 + 1);

        // revert with 0 address as owner or spender
        vm.expectRevert("ERC20: approve from the zero address");
        vm.prank(address(0));
        _testing.increaseAllowance(spender, 0);
        vm.expectRevert("ERC20: approve to the zero address");
        _testing.increaseAllowance(address(0), 0);

        // 3. decreaseAllowance()
        vm.expectEmit(true, true, false, true, address(_testing));
        emit Approval(owner, spender, 1025 - 2);
        vm.prank(owner);
        _testing.decreaseAllowance(spender, 2);
        assertEq(_testing.allowance(owner, spender), 1025 - 2);

        // revert with 0 address as owner or spender
        vm.expectRevert("ERC20: approve from the zero address");
        vm.prank(address(0));
        _testing.decreaseAllowance(spender, 0);
        vm.expectRevert("ERC20: approve to the zero address");
        _testing.decreaseAllowance(address(0), 0);
    }
}

2.4 transfer(address to, uint256 amount) && transferFrom(address from, address to, uint256 amount)

  • transfer(address to, uint256 amount):IERC20中的方法,调用者给to地址转账amount数量的ERC20 token。注:to地址不能为0地址,且调用者的ERC20 token余额必须不可小于转账数量amount;

  • transferFrom(address from, address to, uint256 amount): IERC20中的方法,调用者(spender)将from地址名下数量为amount的ERC20 token转移给to地址。 要求spender具有from的授权数量>=amount。注:转账成功后,spender来自from的授权额度也会同步减少amount。

    function transfer(address to, uint256 amount) public virtual override returns (bool) {
        // 获取调用者地址,即from地址
        address owner = _msgSender();
        // 调用_transfer()方法进行转账的操作
        _transfer(owner, to, amount);
        // 转账成功返回true
        return true;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        // 获取调用者地址(即spender)
        address spender = _msgSender();
        // 调用_spendAllowance()进行授权的相关操作
        _spendAllowance(from, spender, amount);
        // 调用_transfer()进行转账的相关操作
        _transfer(from, to, amount);
        // 转账成功后返回true
        return true;
    }

    // 从from名下转移数量为amount的ERC20 token给to地址。可以在该internal函数中根据业务需求增加转账扣费或代币削减机制。
    // 注:
    // 1. from和to都不可为0地址
    // 2. from的余额要>=amount
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        // 要求from地址不为0地址
        require(from != address(0), "ERC20: transfer from the zero address");
        // 要求to地址不为0地址
        require(to != address(0), "ERC20: transfer to the zero address");

    // 执行hook函数_beforeTokenTransfer(),可在该方法中自定义转账前操作逻辑
        _beforeTokenTransfer(from, to, amount);

    // 获取from当前余额
        uint256 fromBalance = _balances[from];
        // 要求from的余额足以支付这比转账
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        // 关闭solidity 0.8版本的安全数学检查
        unchecked {
            // 更新from转账后余额
            _balances[from] = fromBalance - amount;
            // 更新to转账后余额。该处加法一定不会发生溢出,因为ERC20 token的发行总量是固定不变的,该处加法的结果一定会被控制在total supply之内
            _balances[to] += amount;
        }

    // 抛出Transfer事件
        emit Transfer(from, to, amount);
    // 执行hook函数_afterTokenTransfer(),可在该方法中自定义转账后操作逻辑
        _afterTokenTransfer(from, to, amount);
    }

    // spender消耗掉来自owner的授权额度,数量为amount
    // 注:
    // 1. spender获得了来自owner的无限授权(即授权额度为type(uint256).max),在此函数中将无任何更新;
    // 2. 如果spender来自owner的授权额度<amount,revert
    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        // 获取spender当前来自owner的授权额度
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            // 如果spender当前来自owner的授权额度不是type(uint256).max
            // 要求当前额度值>=amount
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            // 关闭solidity 0.8版本的安全数学检查
            unchecked {
                // 更新spender来自owner的授权额度为currentAllowance - amount,即原额度消耗掉amount
                _approve(owner, spender, currentAllowance - amount);
            }
        }
    }

foundry代码验证:

contract ERC20Test is Test {
    MockERC20 private _testing = new MockERC20("test name", "test symbol");
    address private owner = address(1);
    address private spender = address(2);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);

    function test_TransferAndTransferFrom() external {
        // 1. transfer()
        address to = address(3);
        _testing.mint(owner, 100);
        vm.expectEmit(true, true, false, true, address(_testing));
        emit Transfer(owner, to, 1);
        vm.prank(owner);
        _testing.transfer(to, 1);
        assertEq(_testing.balanceOf(owner), 100 - 1);
        assertEq(_testing.balanceOf(to), 1);

        // revert with 0 address as from or to
        vm.expectRevert("ERC20: transfer from the zero address");
        vm.prank(address(0));
        _testing.transfer(to, 0);
        vm.expectRevert("ERC20: transfer to the zero address");
        _testing.transfer(address(0), 0);

        // revert with insufficient balance
        vm.expectRevert("ERC20: transfer amount exceeds balance");
        vm.prank(owner);
        _testing.transfer(to, 99 + 1);

        // 2. transferFrom()
        // revert if allowance < amount
        vm.prank(owner);
        _testing.approve(spender, 10);
        vm.expectRevert("ERC20: insufficient allowance");
        vm.prank(spender);
        _testing.transferFrom(owner, to, 11);

        // revert if amount > owner's balance
        uint balance = _testing.balanceOf(owner);
        vm.prank(owner);
        _testing.approve(spender, balance + 1);
        vm.expectRevert("ERC20: transfer amount exceeds balance");
        vm.prank(spender);
        _testing.transferFrom(owner, to, balance + 1);

        // revert with 0 address as owner or spender or to
        vm.expectRevert("ERC20: approve from the zero address");
        _testing.transferFrom(address(0), to, 0);

        vm.prank(address(0));
        vm.expectRevert("ERC20: approve to the zero address");
        _testing.transferFrom(owner, to, 0);

        vm.expectRevert("ERC20: transfer to the zero address");
        vm.prank(spender);
        _testing.transferFrom(owner, address(0), 0);

        // pass with emit
        uint balanceOwner = _testing.balanceOf(owner);
        uint balanceTo = _testing.balanceOf(to);
        vm.prank(owner);
        _testing.approve(spender, 10);

        vm.expectEmit(true, true, false, true, address(_testing));
        emit Approval(owner, spender, 10 - 9);
        emit Transfer(owner, to, 9);
        vm.prank(spender);
        _testing.transferFrom(owner, to, 9);
        assertEq(_testing.balanceOf(owner), balanceOwner - 9);
        assertEq(_testing.balanceOf(to), balanceTo + 9);

        // no approval update with infinite allowance
        vm.prank(owner);
        _testing.approve(spender, type(uint).max);
        assertEq(_testing.allowance(owner, spender), type(uint).max);

        vm.prank(spender);
        _testing.transferFrom(owner, to, 10);
        assertEq(_testing.allowance(owner, spender), type(uint).max);
    }
}

ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

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

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

0 条评论

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