Alert Source Discuss
Standards Track: ERC

ERC-3156: 闪电贷

Authors Alberto Cuesta Cañada (@alcueca), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu), Austin Williams (@onewayfunction)
Created 2020-11-15

简述

此 ERC 提供单一资产闪电贷的标准接口和流程。

摘要

闪电贷是一种智能合约交易,其中贷款方智能合约向借款方智能合约提供资产,条件是在交易结束前归还资产,并支付可选费用。此 ERC 指定了贷款方接受闪电贷请求以及借款方在贷款方执行过程中临时控制交易的接口。 还指定了安全执行闪电贷的流程。

动机

闪电贷允许智能合约借出一定数量的代币,而无需抵押,但前提是必须在同一交易中归还。

早期采用闪电贷模式的人员已经产生了不同的接口和不同的使用模式。 预计这种多样化将会加剧,随之而来的是与各种闪电贷模式集成所需的技术负担。

跨协议的方法的一些高级差异包括:

  • 交易结束时的还款方式,有些从贷款接收方提取本金加费用,有些则需要贷款接收方手动将本金和费用返还给贷款方。
  • 一些贷款方能够使用与最初借入的代币不同的代币来偿还贷款,这可以降低闪电交易的整体复杂性和 gas 费用。
  • 一些贷款方提供进入协议的单一入口点,无论您是购买、出售、存入还是将它们链接在一起作为闪电贷,而其他协议则提供离散的入口点。
  • 一些贷款方允许闪电增发任意数量的本地代币而不收取费用,从而有效地允许受计算约束而不是资产所有权约束的闪电贷。

规范

闪电贷功能使用回调模式集成两个智能合约。 在此 EIP 中,这些分别称为 LENDER 和 RECEIVER。

贷款方规范

lender 必须实现 IERC3156FlashLender 接口。

pragma solidity ^0.7.0 || ^0.8.0;
import "./IERC3156FlashBorrower.sol";


interface IERC3156FlashLender {

    /**
     * @dev 可供借出的货币数量。
     * @param token 贷款货币。
     * @return 可以借入的 `token` 数量。
     */
    function maxFlashLoan(
        address token
    ) external view returns (uint256);

    /**
     * @dev 给定贷款收取的费用。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @return 贷款收取的 `token` 数量,在本金之上。
     */
    function flashFee(
        address token,
        uint256 amount
    ) external view returns (uint256);

    /**
     * @dev 发起闪电贷。
     * @param receiver 贷款中代币的接收者,也是回调的接收者。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @param data 任意数据结构,旨在包含用户定义的参数。
     */
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external returns (bool);
}

maxFlashLoan 函数必须返回 token 的最大贷款额。 如果当前不支持 token,则 maxFlashLoan 必须返回 0,而不是revert。

flashFee 函数必须返回 amount token 贷款收取的费用。 如果不支持该代币,则 flashFee 必须 revert。

flashLoan 函数必须在 IERC3156FlashBorrower 合同中包含对 onFlashLoan 函数的回调。

function flashLoan(
    IERC3156FlashBorrower receiver,
    address token,
    uint256 amount,
    bytes calldata data
) external returns (bool) {
  ...
  require(
      receiver.onFlashLoan(msg.sender, token, amount, fee, data) == keccak256("ERC3156FlashBorrower.onFlashLoan"),
      "IERC3156: Callback failed"
  );
  ...
}

flashLoan 函数必须在回调到接收者之前将 amounttoken 转移到 receiver

flashLoan 函数必须包含 msg.sender 作为 initiatoronFlashLoan

flashLoan 函数不得修改收到的 tokenamountdata 参数,并且必须将它们传递给 onFlashLoan

flashLoan 函数必须包含一个 fee 参数到 onFlashLoan,其中包含支付贷款的费用,在本金之上,确保 fee == flashFee(token, amount)

lender 必须验证 onFlashLoan 回调是否返回“ERC3156FlashBorrower.onFlashLoan”的 keccak256 哈希值。

回调后,flashLoan 函数必须从 receiver 中获取 amount + fee token,否则如果失败则 revert。

如果成功,flashLoan 必须返回 true

接收方规范

闪电贷的 receiver 必须实现 IERC3156FlashBorrower 接口:

pragma solidity ^0.7.0 || ^0.8.0;


interface IERC3156FlashBorrower {

    /**
     * @dev 接收闪电贷。
     * @param initiator 贷款的发起者。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @param fee 要偿还的额外代币数量。
     * @param data 任意数据结构,旨在包含用户定义的参数。
     * @return "ERC3156FlashBorrower.onFlashLoan" 的 keccak256 哈希值
     */
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32);
}

为了使交易不 revert,receiver 必须在 onFlashLoan 结束之前批准由 msg.sender 获取的 amount + feetoken

如果成功,onFlashLoan 必须返回“ERC3156FlashBorrower.onFlashLoan”的 keccak256 哈希值。

原理

选择此 ERC 中描述的接口是为了涵盖已知的闪电贷用例,同时允许安全且节省 gas 的实施。

flashFee 在不受支持的代币上 revert,因为返回数值不正确。

选择 flashLoan 作为函数名称,因为它具有足够的描述性,不太可能与贷款方中的其他函数冲突,并且包括贷款方持有或增发代币的用例。

receiver 作为参数,以允许在单独的贷款发起者和接收者的实现上具有灵活性。

现有的闪电贷方都从同一合约提供多种代币类型的闪电贷。 在 flashLoanonFlashLoan 函数中提供 token 参数与观察到的功能非常匹配。

包含 bytes calldata data 参数,供调用者将任意信息传递给 receiver,而不会影响 flashLoan 标准的效用。

选择 onFlashLoan 作为函数名称,因为它具有足够的描述性,不太可能与 receiver 中的其他函数冲突,并且遵循 EIP-667 中也使用的 onAction 命名模式。

onFlashLoan 函数中通常需要 initiator,贷款方将其称为 msg.sender。 另一种实现方式是将 initiator 嵌入调用方的 data 参数中,这将需要额外的机制供接收方验证其准确性,因此不建议这样做。

onFlashLoan 函数中将需要 amount,贷款方将其作为参数。 另一种实现方式是将 amount 嵌入调用方的 data 参数中,这将需要额外的机制供接收方验证其准确性,因此不建议这样做。

通常会在 flashLoan 函数中计算 feereceiver 必须知道此信息才能进行还款。 将 fee 作为参数传递而不是附加到 data 简单有效。

amount + fee 是从 receiver 中提取的,以允许 lender 实现其他依赖于使用 transferFrom 的功能,而无需在闪电贷期间锁定它们。 另一种实现方式是将还款转移到 lender,但这需要贷款方中的所有其他功能也基于使用 transfer 而不是 transferFrom。 鉴于“提取”架构比“推送”架构的复杂性更低且更为普遍,因此选择了“提取”。

向后兼容性

未发现向后兼容性问题。

实施

闪电贷者参考实现

pragma solidity ^0.8.0;

import "./interfaces/IERC20.sol";
import "./interfaces/IERC3156FlashBorrower.sol";
import "./interfaces/IERC3156FlashLender.sol";


contract FlashBorrower is IERC3156FlashBorrower {
    enum Action {NORMAL, OTHER}

    IERC3156FlashLender lender;

    constructor (
        IERC3156FlashLender lender_
    ) {
        lender = lender_;
    }

    /// @dev ERC-3156 闪电贷回调
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external override returns(bytes32) {
        require(
            msg.sender == address(lender),
            "FlashBorrower: Untrusted lender"
        );
        require(
            initiator == address(this),
            "FlashBorrower: Untrusted loan initiator"
        );
        (Action action) = abi.decode(data, (Action));
        if (action == Action.NORMAL) {
            // 做一件事
        } else if (action == Action.OTHER) {
            // 做另一件事
        }
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

    /// @dev 发起闪电贷
    function flashBorrow(
        address token,
        uint256 amount
    ) public {
        bytes memory data = abi.encode(Action.NORMAL);
        uint256 _allowance = IERC20(token).allowance(address(this), address(lender));
        uint256 _fee = lender.flashFee(token, amount);
        uint256 _repayment = amount + _fee;
        IERC20(token).approve(address(lender), _allowance + _repayment);
        lender.flashLoan(this, token, amount, data);
    }
}

闪电增发参考实现

pragma solidity ^0.8.0;

import "../ERC20.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/IERC3156FlashBorrower.sol";
import "../interfaces/IERC3156FlashLender.sol";


/**
 * @author Alberto Cuesta Cañada
 * @dev {ERC20} 的扩展,允许闪电增发。
 */
contract FlashMinter is ERC20, IERC3156FlashLender {

    bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
    uint256 public fee; //  1 == 0.01 %.

    /**
     * @param fee_ 需要偿还的贷款 `amount` 的百分比,在本金之外。
     */
    constructor (
        string memory name,
        string memory symbol,
        uint256 fee_
    ) ERC20(name, symbol) {
        fee = fee_;
    }

    /**
     * @dev 可供借出的货币数量。
     * @param token 贷款货币。
     * @return 可以借入的 `token` 数量。
     */
    function maxFlashLoan(
        address token
    ) external view override returns (uint256) {
        return type(uint256).max - totalSupply();
    }

    /**
     * @dev 给定贷款收取的费用。
     * @param token 贷款货币。 必须与此合约的地址匹配。
     * @param amount 借出的代币数量。
     * @return 贷款收取的 `token` 数量,在本金之外。
     */
    function flashFee(
        address token,
        uint256 amount
    ) external view override returns (uint256) {
        require(
            token == address(this),
            "FlashMinter: Unsupported currency"
        );
        return _flashFee(token, amount);
    }

    /**
     * @dev 向 `receiver` 贷款 `amount` 个代币,并在 ERC3156 回调后取回,外加 `flashFee`。
     * @param receiver 接收代币的合约,需要实现 `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` 接口。
     * @param token 贷款货币。 必须与此合约的地址匹配。
     * @param amount 借出的代币数量。
     * @param data 要传递给 `receiver` 以供任何自定义用途的数据参数。
     */
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external override returns (bool){
        require(
            token == address(this),
            "FlashMinter: Unsupported currency"
        );
        uint256 fee = _flashFee(token, amount);
        _mint(address(receiver), amount);
        require(
            receiver.onFlashLoan(msg.sender, token, amount, fee, data) == CALLBACK_SUCCESS,
            "FlashMinter: Callback failed"
        );
        uint256 _allowance = allowance(address(receiver), address(this));
        require(
            _allowance >= (amount + fee),
            "FlashMinter: Repay not approved"
        );
        _approve(address(receiver), address(this), _allowance - (amount + fee));
        _burn(address(receiver), amount + fee);
        return true;
    }

    /**
     * @dev 给定贷款收取的费用。 没有检查的内部函数。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @return 贷款收取的 `token` 数量,在本金之外。
     */
    function _flashFee(
        address token,
        uint256 amount
    ) internal view returns (uint256) {
        return amount * fee / 10000;
    }
}

闪电贷参考实现

pragma solidity ^0.8.0;

import "../interfaces/IERC20.sol";
import "../interfaces/IERC3156FlashBorrower.sol";
import "../interfaces/IERC3156FlashLender.sol";


/**
 * @author Alberto Cuesta Cañada
 * @dev {ERC20} 的扩展,允许闪电贷。
 */
contract FlashLender is IERC3156FlashLender {

    bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
    mapping(address => bool) public supportedTokens;
    uint256 public fee; //  1 == 0.01 %.


    /**
     * @param supportedTokens_ 闪电贷支持的代币合约。
     * @param fee_ 需要偿还的贷款 `amount` 的百分比,在本金之外。
     */
    constructor(
        address[] memory supportedTokens_,
        uint256 fee_
    ) {
        for (uint256 i = 0; i < supportedTokens_.length; i++) {
            supportedTokens[supportedTokens_[i]] = true;
        }
        fee = fee_;
    }

    /**
     * @dev 向 `receiver` 贷款 `amount` 个代币,并在回调后取回,外加 `flashFee`。
     * @param receiver 接收代币的合约,需要实现 `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` 接口。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @param data 要传递给 `receiver` 以供任何自定义用途的数据参数。
     */
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external override returns(bool) {
        require(
            supportedTokens[token],
            "FlashLender: Unsupported currency"
        );
        uint256 fee = _flashFee(token, amount);
        require(
            IERC20(token).transfer(address(receiver), amount),
            "FlashLender: Transfer failed"
        );
        require(
            receiver.onFlashLoan(msg.sender, token, amount, fee, data) == CALLBACK_SUCCESS,
            "FlashLender: Callback failed"
        );
        require(
            IERC20(token).transferFrom(address(receiver), address(this), amount + fee),
            "FlashLender: Repay failed"
        );
        return true;
    }

    /**
     * @dev 给定贷款收取的费用。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @return 贷款收取的 `token` 数量,在本金之外。
     */
    function flashFee(
        address token,
        uint256 amount
    ) external view override returns (uint256) {
        require(
            supportedTokens[token],
            "FlashLender: Unsupported currency"
        );
        return _flashFee(token, amount);
    }

    /**
     * @dev 给定贷款收取的费用。 没有检查的内部函数。
     * @param token 贷款货币。
     * @param amount 借出的代币数量。
     * @return 贷款收取的 `token` 数量,在本金之外。
     */
    function _flashFee(
        address token,
        uint256 amount
    ) internal view returns (uint256) {
        return amount * fee / 10000;
    }

    /**
     * @dev 可供借出的货币数量。
     * @param token 贷款货币。
     * @return 可以借入的 `token` 数量。
     */
    function maxFlashLoan(
        address token
    ) external view override returns (uint256) {
        return supportedTokens[token] ? IERC20(token).balanceOf(address(this)) : 0;
    }
}

安全考虑事项

验证回调参数

onFlashLoan 的参数预计会反映闪电贷的条件,但不能无条件信任。 它们可以分为两组,在可以信任它们是真实的之前,需要进行不同的检查。

  1. 如果没有某种验证,则不能假定任何参数都是真实的。 如果 onFlashLoan 的调用方决定撒谎,则 initiatortokenamount 指的是可能没有发生的过去交易。 fee 可能是假的或计算不正确。 data 可能已被调用方篡改。
  2. 要信任 initiatortokenamountfee 的值是真实的,一个合理的模式是验证 onFlashLoan 调用方是否在经过验证的闪电贷方的白名单中。 由于 flashLoan 的调用方通常也会收到 onFlashLoan 回调,这将非常简单。 在所有其他情况下,如果要信任 onFlashLoan 中的参数,则需要批准闪电贷方。
  3. 要信任 data 的值是真实的,除了第 1 点中的检查之外,还建议验证 initiator 是否属于一组受信任的地址。 信任 lenderinitiator 就足以信任 data 的内容是真实的。

闪电贷安全注意事项

自动批准

最安全的方法是在执行 flashLoan 之前实现 amount+fee 的批准。

任何为给定的 lender 保留批准的 receiver 必须在 onFlashLoan 中包含一个机制来验证发起者是否受信任。

任何在 onFlashLoan 中包含批准 lender 获取 amount + feereceiver 都需要与验证发起者是否受信任的机制相结合。

如果一个具有非 revert 回退功能或 EOA 的不知情合约批准了一个实现 ERC3156 的 lender,并且没有立即使用该批准,并且如果 lender 没有验证 onFlashLoan 的返回值,那么不知情的合约或 EOA 可能会被耗尽资金,直至达到其许可或余额限制。 这将由 initiator 在受害者身上调用 flashLoan 来执行。 将执行并偿还闪电贷,外加任何费用,这些费用将由 lender 累积。 因此,重要的是 lender 完整地实现规范,并且如果 onFlashLoan 没有返回“ERC3156FlashBorrower.onFlashLoan”的 keccak256 哈希值,则 revert。

闪电增发外部安全注意事项

闪电增发交易中涉及的典型代币数量将产生新的创新攻击向量。

示例 1 - 利率攻击

如果存在一个提供稳定利率的贷款协议,但它没有最低/最高利率限制,并且它没有根据闪电引起的流动性变化重新平衡固定利率,那么它可能会受到以下情况的影响:

FreeLoanAttack.sol

  1. 闪电增发 1 quintillion STAB
  2. 存入 1 quintillion STAB + 价值 150 万美元的 ETH 抵押品
  3. 您的总存款数量现在将稳定利率推低至 0.00001% 的稳定利率
  4. 根据 150 万美元的 ETH 抵押品,以 0.00001% 的稳定利率借入 100 万个 STAB
  5. 提取并销毁 1 quint STAB 以结束原始闪电增发
  6. 您现在拥有一笔 100 万 STAB 的贷款,实际上是永久免息的(每年 0.10 美元的利息)

关键的结论是,显然需要实施一个固定的最低/最高利率限制,并根据短期流动性变化重新平衡利率。

示例 2 - 算术溢出和下溢

如果闪电增发提供商对交易中可闪电增发的代币数量没有任何限制,那么任何人都可以闪电增发 2^256-1 个代币。

闪电增发接收端的协议将需要确保他们的合约可以处理这个问题,要么使用在智能合约字节码中嵌入溢出保护的编译器,要么设置显式检查。

闪电增发内部安全注意事项

在同一平台中将闪电增发与特定于业务的功能相结合很容易导致意想不到的后果。

示例 - 国库耗尽

假设一个智能合约闪电贷其原生代币。 当用户销毁原生代币时,同一个智能合约会从第三方借款。 这种模式将用于将多个用户的抵押债务聚合到第三方中的单个帐户中。 闪电增发可用于导致贷款方借款到其限额,然后推高底层贷款方的利率,清算闪电贷方:

  1. lender 闪电增发大量 FOO。
  2. 将 FOO 兑换为 BAR,导致 lenderunderwriter 借款到其借款限额。
  3. 触发 underwriter 中的债务利率增加,使 lender 抵押不足。
  4. 清算 lender 以获取利润。

版权

Copyright 和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Alberto Cuesta Cañada (@alcueca), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu), Austin Williams (@onewayfunction), "ERC-3156: 闪电贷," Ethereum Improvement Proposals, no. 3156, November 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3156.