ERC20FlashMint库是ERC20的拓展。本库在ERC20的基础上实现了IERC3156FlashLender接口,在token层面上支持了闪电贷功能。但是该库默认没有闪电贷手续费,开发者可以通过重写flashFee()方法来自定义手续费计算逻辑。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
ERC20FlashMint库是ERC20的拓展,也是关于闪电贷ERC3156的实现。ERC20FlashMint库在ERC20的基础上实现了IERC3156FlashLender接口,在token层面上支持了闪电贷功能。但是该库默认没有闪电贷手续费,开发者可以通过重写flashFee()方法来自定义手续费计算逻辑。
EIP3156详情参见:https://eips.ethereum.org/EIPS/eip-3156
继承ERC20FlashMint合约:
// 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测试合约:
测试使用的物料合约:
// 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;
    }
}
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);
    }
}
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);
    }
}
执行闪电贷。如果成功执行,返回true。
本ERC20合约会mint出新的token给receiver地址。闪电贷结束时需要满足如下条件才算成功:
    // 参数:
    // - 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神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
