Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-3234: 批量闪电贷

Authors Alberto Cuesta Cañada (@albertocuestacanada), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu), Austin Williams (@onewayfunction)
Created 2021-01-31
Discussion Link https://ethereum-magicians.org/t/erc-3234-batch-flash-loans/5271

简单总结

本 ERC 提供了多资产闪电贷的标准接口和流程。

动机

多资产闪电贷,或批量闪电贷,是闪电贷提供商的常见产品,并且在同时对平台之间的多个仓位进行再融资方面具有强大的用例。与此同时,批量闪电贷比单资产闪电贷 (ER3156) 更复杂。用例和用户配置文件的这种差异需要针对单资产闪电贷和批量闪电贷的独立但一致的标准。

规范

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

贷款人规范

lender 必须实现 IERC3234BatchFlashLender 接口。

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


interface IERC3234BatchFlashLender {

    /**
     * @dev 可供借出的货币数量。
     * @param tokens 批量贷款中每笔贷款的货币。
     * @return 每笔贷款可以借入的最大金额。
     */
    function maxFlashLoan(
        address[] calldata tokens
    ) external view returns (uint256[]);

    /**
     * @dev 特定批量贷款将收取的费用。
     * @param tokens 贷款货币。
     * @param amounts 借出的 token 金额。
     * @return 每笔 `token` 的金额,作为每笔贷款的费用收取,在返还本金的基础上。
     */
    function flashFee(
        address[] calldata tokens,
        uint256[] calldata amounts
    ) external view returns (uint256[]);

    /**
     * @dev 启动批量闪电贷。
     * @param receiver 贷款中 token 的接收者,以及回调的接收者。
     * @param tokens 贷款货币。
     * @param amounts 借出的 token 金额。
     * @param data 任意数据结构,旨在包含用户定义的参数。
     */
    function batchFlashLoan(
        IERC3234BatchFlashBorrower receiver,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bytes[] calldata data
    ) external returns (bool);
}

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

flashFee 函数必须返回对 amount token 的每笔贷款收取的费用。如果不支持某个 token,则 flashFee 必须 revert。

batchFlashLoan 函数必须包含对 IERC3234BatchFlashBorrower 合约中 onBatchFlashLoan 函数的回调。

function batchFlashLoan(
    IERC3234BatchFlashBorrower receiver,
    address[] calldata tokens,
    uint256[] calldata amounts,
    bytes calldata data
) external returns (bool) {
  ...
    require(
        receiver.onBatchFlashLoan(
            msg.sender,
            tokens,
            amounts,
            fees,
            data
        ) == keccak256("ERC3234BatchFlashBorrower.onBatchFlashLoan"),
        "IERC3234: Callback failed"
    );
  ...
}

batchFlashLoan 函数必须在回调借款人之前将每个 tokens[i]amounts[i] 转移到 receiver

batchFlashLoan 函数必须包含 msg.sender 作为 onBatchFlashLoaninitiator

batchFlashLoan 函数不得修改收到的 tokensamountsdata 参数,并且必须将其传递给 onBatchFlashLoan

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

batchFlashLoan 函数必须包含一个 fees 参数到 onBatchFlashLoan,其中包含为每个单独的 tokenamount 支付的费用,确保 fees[i] == flashFee(tokens[i], amounts[i])

回调之后,对于 tokens 中的每个 tokenbatchFlashLoan 函数必须从 receiver 中获取 tokens[i]amounts[i] + fees[i],否则如果失败,则 revert。

如果成功,batchFlashLoan 必须返回 true

接收方规范

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

pragma solidity ^0.7.0 || ^0.8.0;


interface IERC3234BatchFlashBorrower {

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

为了使交易不 revert,对于 tokens 中的每个 tokenreceiver 必须在 onBatchFlashLoan 结束之前批准 msg.sender 获取 tokens[i]amounts[i] + fees[i]

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

合理依据

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

flashFee 在不支持的 token 上 revert,因为返回一个数值是不正确的。

batchFlashLoan 已被选为一个函数名称,因为它具有足够的描述性,不太可能与 lender 中的其他函数冲突,并且包括 lender 持有或铸造 token 的用例。

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

现有的闪电贷提供商(Aave、dYdX 和 Uniswap)都从同一合约(LendingPool、SoloMargin 和 UniswapV2Pair)提供几种 token 类型的闪电贷。在 batchFlashLoanonBatchFlashLoan 函数中都提供 token 参数与观察到的功能非常匹配。

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

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

initiator 通常在 onBatchFlashLoan 函数中是必需的,lender 将其知道为 msg.sender。另一种实现方式是将 initiator 嵌入到调用者的 data 参数中,这将需要额外的机制供 receiver 验证其准确性,因此不建议这样做。

amounts 将在 onBatchFlashLoan 函数中是必需的,lender 将其作为参数。另一种实现方式是将 amounts 嵌入到调用者的 data 参数中,这将需要额外的机制供 receiver 验证其准确性,因此不建议这样做。

fees 通常在 batchFlashLoan 函数中计算,receiver 必须知道这些费用才能进行偿还。将 fees 作为参数传递而不是附加到 data 简单有效。

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

安全注意事项

验证回调参数

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

  1. 在没有某种验证的情况下,不能假定任何参数都是真实的。如果 onBatchFlashLoan 的调用者决定说谎,initiatortokensamounts 将指向可能没有发生的过去交易。fees 可能是假的或计算不正确的。data 可能已被调用者操纵。
  2. 为了信任 initiatortokensamountsfees 的值是真实的,一个合理的模式是验证 onBatchFlashLoan 调用者是否在经过验证的闪电贷提供商的白名单中。由于通常 batchFlashLoan 的调用者也将收到 onBatchFlashLoan 回调,因此这将是微不足道的。在所有其他情况下,如果要信任 onBatchFlashLoan 中的参数,则需要批准闪电贷提供商。
  3. 为了信任 data 的值是真实的,除了第 1 点中的检查之外,还建议 receiver 验证 initiator 是否在某个受信任地址列表中。信任 lenderinitiator 足以信任 data 的内容是真实的。

闪电贷安全注意事项

自动批准不受信任的借款人

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

onBatchFlashLoan 中包含对 lender 获取 amount + fee 的批准需要与验证借款人是否受信任的机制相结合,例如上面描述的那些。

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

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

闪电增发交易中涉及的典型 token 量将产生新的创新攻击媒介。

示例 1 - 利率攻击

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

FreeLoanAttack.sol

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

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

示例 2 - 算术溢出和下溢

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

闪电增发的接收端协议将需要确保他们的合约可以处理这个问题。一种显而易见的方法是利用 OpenZeppelin 的 SafeMath 库作为一个全面的安全网,但是应该考虑到何时使用或不使用它,考虑到 gas 的权衡。

如果您还记得,在 2018 年发生了一系列事件,其中 OKEx、Poloniex、HitBTC 和 Huobi 等交易所不得不关闭 ERC20 token 的存款和取款,因为 ERC20 token 合约中存在整数溢出。

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

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

示例 - 国库耗尽

在 Yield Protocol 的早期实现中,闪电贷的 fyDai 可以兑换成 Dai,这可以用来清算 MakerDAO 中的 Yield Protocol CDP 金库:

  1. 闪电增发大量的 fyDai。
  2. 兑换尽可能多的 fyDai 的 Dai,因为 Yield Protocol 抵押品允许。
  3. 通过调用 jug.drip 触发稳定费率增加,这将使 Yield Protocol 没有抵押品。
  4. 清算 MakerDAO 中的 Yield Protocol CDP 金库。

版权

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

Citation

Please cite this document as:

Alberto Cuesta Cañada (@albertocuestacanada), Fiona Kobayashi (@fifikobayashi), fubuloubu (@fubuloubu), Austin Williams (@onewayfunction), "ERC-3234: 批量闪电贷 [DRAFT]," Ethereum Improvement Proposals, no. 3234, January 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3234.