Michael.W基于Foundry精读Openzeppelin第45期——ERC20FlashMint.sol

  • Michael.W
  • 更新于 2023-12-26 22:10
  • 阅读 2613

ERC20FlashMint库是ERC20的拓展。本库在ERC20的基础上实现了IERC3156FlashLender接口,在token层面上支持了闪电贷功能。但是该库默认没有闪电贷手续费,开发者可以通过重写flashFee()方法来自定义手续费计算逻辑。

0. 版本

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

0.1 ERC20FlashMint.sol

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

ERC20FlashMint库是ERC20的拓展,也是关于闪电贷ERC3156的实现。ERC20FlashMint库在ERC20的基础上实现了IERC3156FlashLender接口,在token层面上支持了闪电贷功能。但是该库默认没有闪电贷手续费,开发者可以通过重写flashFee()方法来自定义手续费计算逻辑。

EIP3156详情参见:https://eips.ethereum.org/EIPS/eip-3156

1. 目标合约

继承ERC20FlashMint合约:

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

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

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

contract MockERC20FlashMint is ERC20FlashMint {
    bool private _customizedFlashFeeAndReceiver;

    constructor(
        string memory name,
        string memory symbol,
        address richer,
        uint totalSupply
    )
    ERC20(name, symbol)
    {
        _mint(richer, totalSupply);
    }

    function customizedFlashFeeAndReceiver() external {
        _customizedFlashFeeAndReceiver = true;
    }

    // customized flash fee 10% amount
    function _flashFee(address token, uint amount) internal view override returns (uint) {
        return _customizedFlashFeeAndReceiver ?
            amount / 10 : ERC20FlashMint._flashFee(token, amount);
    }

    // customized fee receiver address(1024)
    function _flashFeeReceiver() internal view override returns (address) {
        return _customizedFlashFeeAndReceiver ?
            address(1024) : ERC20FlashMint._flashFeeReceiver();
    }
}

全部foundry测试合约:

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

测试使用的物料合约:

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

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

import "openzeppelin-contracts/contracts/interfaces/IERC3156FlashBorrower.sol";
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract ERC3156FlashBorrower is IERC3156FlashBorrower {
    bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");

    bool private _enableApprove;
    bool private _enableValidReturnValue;

    event ParamsIn(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes data
    );

    event Monitor(
        address owner,
        uint balance,
        uint totalSupply
    );

    // implementation of IERC3156FlashBorrower.onFlashLoan()
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32){
        IERC20 erc20Token = IERC20(token);
        // show the params input
        emit ParamsIn(
            initiator,
            token,
            amount,
            fee,
            data
        );

        // show the token status during IERC3156FlashBorrower.onFlashLoan()
        emit Monitor(
            address(this),
            erc20Token.balanceOf(address(this)),
            erc20Token.totalSupply()
        );

        if (data.length != 0) {
            (bool ok,) = token.call(data);
            require(ok, "fail to call");
        }

        if (_enableApprove) {
            erc20Token.approve(token, amount + fee);
        }

        return _enableValidReturnValue ? _RETURN_VALUE : bytes32(0);
    }

    function flipApprove() external {
        _enableApprove = !_enableApprove;
    }

    function flipValidReturnValue() external {
        _enableValidReturnValue = !_enableValidReturnValue;
    }
}

2. 代码精读

2.1 maxFlashLoan(address token)

IERC3156FlashLender中的标准方法实现,返回输入token最大可借贷的数量。

    // 如果IERC3156FlashBorrower.onFlashLoan()方法返回该常量值表示该方法执行有效
    bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan");

    // 参数:
    // - token: 要借出token的地址
    function maxFlashLoan(address token) public view virtual override returns (uint256) {
        // 如果传入的token地址为本ERC20地址,返回最大可借贷数量为type(uint256).max-当前本ERC20的总供应量。否则返回0
        return token == address(this) ? type(uint256).max - ERC20.totalSupply() : 0;
    }

foundry代码验证:

contract ERC20FlashMintTest is Test {
    MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);

    function test_MaxFlashLoan() external {
        uint totalSupply = _testing.totalSupply();
        assertEq(totalSupply, 10000);
        // query for self
        assertEq(_testing.maxFlashLoan(address(_testing)), type(uint).max - totalSupply);
        // query for other
        assertEq(_testing.maxFlashLoan(address(0)), 0);
    }
}

2.2 flashFee(address token, uint256 amount)

IERC3156FlashLender中的标准方法实现,返回借出数量为amount、地址为token的ERC20需要支付的手续费。该方法内部调用internal方法_flashFee(),可以在子合约中重写_flashFee()方法来实现需要的手续费计算逻辑。

    // 参数:
    // - token: 闪电贷的token地址
    // - amount: 闪电贷的token数量
    function flashFee(address token, uint256 amount) public view virtual override returns (uint256) {
        // 要求token为本ERC20合约地址
        require(token == address(this), "ERC20FlashMint: wrong token");
        // 调用internal方法_flashFee(),返回对应手续费数量
        return _flashFee(token, amount);
    }

    // internal方法,返回具体的借出数量为amount、地址为token的ERC20需要支付的手续费。在子合约中重写此方法,可自定义闪电贷手续费计算逻辑。同时也可以使得本ERC20具备通缩属性
    // - token: 闪电贷的token地址
    // - amount: 闪电贷的token数量
    function _flashFee(address token, uint256 amount) internal view virtual returns (uint256) {
        // 在不添加字节码的情况下,使得未使用传入变量在编译时不再报warning
        token;
        amount;
        // 直接返回0。
        // 注:在子合约中重写此方法,可自定义闪电贷手续费计算逻辑
        return 0;
    }

foundry代码验证:

contract ERC20FlashMintTest is Test {
    MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);

    function test_FlashFee() external {
        // case 1: default flash fee (0)
        uint amountToLoan = 100;
        assertEq(_testing.flashFee(address(_testing), amountToLoan), 0);
        // revert with wrong token address
        vm.expectRevert("ERC20FlashMint: wrong token");
        _testing.flashFee(address(0), amountToLoan);

        // case 2: customized flash fee (10% amountToLoan)
        _testing.customizedFlashFeeAndReceiver();
        assertEq(_testing.flashFee(address(_testing), amountToLoan), amountToLoan / 10);
        // revert with wrong token address
        vm.expectRevert("ERC20FlashMint: wrong token");
        _testing.flashFee(address(0), amountToLoan);
    }
}

2.3 flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)

执行闪电贷。如果成功执行,返回true。

本ERC20合约会mint出新的token给receiver地址。闪电贷结束时需要满足如下条件才算成功:

  1. receiver名下持有至少amount(借贷数量)+ fee(对应手续费)的token;
  2. receiver已授权给本ERC20合约至少如上数量的授权额度,以便本合约可以burn掉receiver名下的token及转移fee。
    // 参数:
    // - receiver: 闪电贷借出token的接受者地址,要求该地址实现了接口IERC3156FlashBorrower
    // - amount: 闪电贷借出token数量
    // - data: 传给receiver,用于执行receiver.onFlashLoan()方法的参数
    // 注:此方法未做重入检查,因为即使重入发生也不会带来风险——因为闪电贷mint出的token在后面都会被burn掉,一旦该平衡被打破整个函数会revert 
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) public virtual override returns (bool) {
        // 要求借贷token数量 <= 最大允许借贷数量,否则revert
        require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan");
        // 计算对应闪电贷手续费
        uint256 fee = flashFee(token, amount);
        // mint给receiver amount数量的token
        _mint(address(receiver), amount);
        // 执行receiver.onFlashLoan()方法,来触发receiver收到贷款后的执行逻辑。要求返回值为常量_RETURN_VALUE,否则revert
        require(
            receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE,
            "ERC20FlashMint: invalid return value"
        );
        // 获取闪电贷手续费接受者地址
        address flashFeeReceiver = _flashFeeReceiver();
        // 消费receiver给本合约的授权额度,即amount(借贷token数量)+ fee(闪电贷手续费)
        _spendAllowance(address(receiver), address(this), amount + fee);
        if (fee == 0 || flashFeeReceiver == address(0)) {
            // 如果手续费为0或者闪电贷手续费接受者地址为0地址,直接销毁该receiver名下amount+fee数量的token
            _burn(address(receiver), amount + fee);
        } else {
            // 如果手续费不为0且闪电贷手续费接受者地址不为0地址
            // 销毁receiver名下数量为amount的token
            _burn(address(receiver), amount);
            // 从receiver名下转移fee数量的手续费到手续费接受者地址
            _transfer(address(receiver), flashFeeReceiver, fee);
        }
        // 返回true
        return true;
    }

    // 返回闪电贷手续费的接受地址。如果该方法返回0地址,表示手续费被天然burn掉。如果需要换成其他地址,可以在子合约中重写该函数
    function _flashFeeReceiver() internal view virtual returns (address) {
        // 直接返回0地址
        return address(0);
    }

foundry代码验证:

contract ERC20FlashMintTest is Test {
    address private constant CUSTOMIZED_FLASH_FEE_RECEIVER = address(1024);

    MockERC20FlashMint private _testing = new MockERC20FlashMint("test name", "test symbol", address(this), 10000);
    ERC3156FlashBorrower private flashBorrower = new ERC3156FlashBorrower();

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

    event ParamsIn(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes data
    );

    event Monitor(
        address owner,
        uint balance,
        uint totalSupply
    );

    function test_FlashLoan_DefaultFlashFeeAndReceiver() external {
        assertEq(_testing.totalSupply(), 10000);
        // case 1: pass with flash borrower's approval and valid return value
        uint amountToLoan = 20000;
        uint defaultFee = 0;
        flashBorrower.flipApprove();
        flashBorrower.flipValidReturnValue();

        // mint amountToLoan to flashBorrower
        vm.expectEmit(address(_testing));
        emit Transfer(address(0), address(flashBorrower), amountToLoan);
        // check params input in IERC3156FlashBorrower.onFlashLoan()
        vm.expectEmit(address(flashBorrower));
        emit ParamsIn(address(this), address(_testing), amountToLoan, defaultFee, '');
        // check the state during IERC3156FlashBorrower.onFlashLoan()
        vm.expectEmit(address(flashBorrower));
        emit Monitor(address(flashBorrower), amountToLoan, amountToLoan + 10000);
        // burn amountToLoan + fee(0) from flashBorrower
        vm.expectEmit(address(_testing));
        emit Transfer(address(flashBorrower), address(0), amountToLoan + defaultFee);
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        // total supply not changed
        assertEq(_testing.totalSupply(), 10000);

        // case 2: revert if amountToLoan > maxFlashLoan
        uint amountExceedsMaxFlashLoan = _testing.maxFlashLoan(address(_testing)) + 1;
        vm.expectRevert("ERC20FlashMint: amount exceeds maxFlashLoan");
        _testing.flashLoan(flashBorrower, address(_testing), amountExceedsMaxFlashLoan, '');

        // case 3: revert if receiver.onFlashLoan() with invalid return value
        flashBorrower.flipValidReturnValue();
        vm.expectRevert("ERC20FlashMint: invalid return value");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        flashBorrower.flipValidReturnValue();

        // case 4: revert without approval in IERC3156FlashBorrower.onFlashLoan()
        flashBorrower.flipApprove();
        vm.expectRevert("ERC20: insufficient allowance");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        flashBorrower.flipApprove();

        // case 5: revert with different amounts can be minted and burned in onFlashLoan()
        // transfer 1 to address(1) in IERC3156FlashBorrower.onFlashLoan()
        bytes memory data = abi.encodeCall(_testing.transfer, (address(1), 1));
        vm.expectRevert("ERC20: burn amount exceeds balance");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, data);
    }

    function test_FlashLoan_CustomizedFlashFeeAndReceiver() external {
        _testing.customizedFlashFeeAndReceiver();
        assertEq(_testing.balanceOf(address(this)), 10000);
        assertEq(_testing.balanceOf(address(flashBorrower)), 0);
        assertEq(_testing.balanceOf(CUSTOMIZED_FLASH_FEE_RECEIVER), 0);

        // case 1: pass with flash borrower's approval and valid return value
        uint amountToLoan = 20000;
        uint customizedFlashFee = amountToLoan / 10;
        flashBorrower.flipApprove();
        flashBorrower.flipValidReturnValue();
        // transfer flash fee to flash borrower
        _testing.transfer(address(flashBorrower), customizedFlashFee);

        // mint amountToLoan to flashBorrower
        vm.expectEmit(address(_testing));
        emit Transfer(address(0), address(flashBorrower), amountToLoan);
        // check params input in IERC3156FlashBorrower.onFlashLoan()
        vm.expectEmit(address(flashBorrower));
        emit ParamsIn(address(this), address(_testing), amountToLoan, customizedFlashFee, '');
        // check the state during IERC3156FlashBorrower.onFlashLoan()
        vm.expectEmit(address(flashBorrower));
        emit Monitor(address(flashBorrower), amountToLoan + customizedFlashFee, amountToLoan + 10000);
        // burn amountToLoan from flashBorrower
        vm.expectEmit(address(_testing));
        emit Transfer(address(flashBorrower), address(0), amountToLoan);
        // transfer customizedFlashFee to customizedFlashFeeReceiver
        vm.expectEmit(address(_testing));
        emit Transfer(address(flashBorrower), CUSTOMIZED_FLASH_FEE_RECEIVER, customizedFlashFee);

        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        // total supply not changed
        assertEq(_testing.totalSupply(), 10000);
        assertEq(_testing.balanceOf(address(this)), 10000 - customizedFlashFee);
        assertEq(_testing.balanceOf(address(flashBorrower)), 0);
        assertEq(_testing.balanceOf(CUSTOMIZED_FLASH_FEE_RECEIVER), customizedFlashFee);

        // case 2: revert if amountToLoan > maxFlashLoan
        uint amountExceedsMaxFlashLoan = _testing.maxFlashLoan(address(_testing)) + 1;
        vm.expectRevert("ERC20FlashMint: amount exceeds maxFlashLoan");
        _testing.flashLoan(flashBorrower, address(_testing), amountExceedsMaxFlashLoan, '');

        // case 3: revert if receiver.onFlashLoan() with invalid return value
        flashBorrower.flipValidReturnValue();
        vm.expectRevert("ERC20FlashMint: invalid return value");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        flashBorrower.flipValidReturnValue();

        // case 4: revert without approval in IERC3156FlashBorrower.onFlashLoan()
        flashBorrower.flipApprove();
        vm.expectRevert("ERC20: insufficient allowance");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
        flashBorrower.flipApprove();

        // case 5: revert with different amounts can be minted and burned in onFlashLoan()
        // transfer 1 to address(1) in IERC3156FlashBorrower.onFlashLoan()
        bytes memory data = abi.encodeCall(_testing.transfer, (address(1), 1));
        vm.expectRevert("ERC20: burn amount exceeds balance");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, data);

        // case 6: revert with insufficient flash fee
        _testing.transfer(address(flashBorrower), customizedFlashFee - 1);
        vm.expectRevert("ERC20: transfer amount exceeds balance");
        _testing.flashLoan(flashBorrower, address(_testing), amountToLoan, '');
    }
}

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

1.jpeg

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

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

0 条评论

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