はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、複数のトークンを一括で借りる仕組みを提案しているERC3234についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

ERC3234は、複数のトークンを一括で扱うフラッシュローン(バッチ・フラッシュローン)の実装を提案しています。

動機

複数の資産を同時に借りるバッチ・フラッシュローンは、多くのフラッシュローンサービス提供者が採用している一般的な機能です。 特に、DeFiプラットフォーム間で複数のポジションを同時にリファイナンス(借り換え)するケースで有効です。

一方で、単一資産を対象としたフラッシュローン(ERC3156)に比べて、バッチ版はより複雑であり、利用者やユースケースも異なります。 そのため、単一資産用の標準とは別に、バッチ・フラッシュローン専用の標準を設けつつ両者に一貫性を持たせる必要があります。

ERC3156については以下の記事を参考にしてください。

ERC3234では、「LENDER(貸し手)」と「RECEIVER(借り手)」の2つのスマートコントラクト間でコールバック処理によってローンのやり取りを行う仕組みを採用しています。

仕様

Lender

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


interface IERC3234BatchFlashLender {

    /**
     * @dev The amount of currency available to be lended.
     * @param tokens The currency for each loan in the batch.
     * @return The maximum amount that can be borrowed for each loan in the batch.
     */
    function maxFlashLoan(
        address[] calldata tokens
    ) external view returns (uint256[]);

    /**
     * @dev The fees to be charged for a given batch loan.
     * @param tokens The loan currencies.
     * @param amounts The amounts of tokens lent.
     * @return The amount of each `token` to be charged for each loan, on top of the returned principal.
     */
    function flashFee(
        address[] calldata tokens,
        uint256[] calldata amounts
    ) external view returns (uint256[]);

    /**
     * @dev Initiate a batch flash loan.
     * @param receiver The receiver of the tokens in the loan, and the receiver of the callback.
     * @param tokens The loan currencies.
     * @param amounts The amount of tokens lent.
     * @param data Arbitrary data structure, intended to contain user-defined parameters.
     */
    function batchFlashLoan(
        IERC3234BatchFlashBorrower receiver,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bytes[] calldata data
    ) external returns (bool);
}

Lenderコントラクトは、IERC3234BatchFlashLender インターフェースを実装する必要があります。 このインターフェースは以下の3つの関数の実装が必要になります。

maxFlashLoan
js
function maxFlashLoan(address[] calldata tokens) external view returns (uint256[]);
  • 指定された各トークンに対して、最大で貸し出し可能な金額を返す関数。
  • 非対応のトークンに対しては 0 を返します。

flashFee

js
function flashFee(address[] calldata tokens, uint256[] calldata amounts) external view returns (uint256[]);
  • 各トークンと金額の組み合わせに対する手数料を返す関数。
  • 非対応のトークンが指定された場合は revert する必要があります。

batchFlashLoan

js
function batchFlashLoan(
    IERC3234BatchFlashBorrower receiver,
    address[] calldata tokens,
    uint256[] calldata amounts,
    bytes[] calldata data
) external returns (bool);
  • 指定された借り手(receiver)に対して、複数のトークンを一括で貸し出しだす関数。
処理の流れ
  1. tokens[i] ごとに amounts[i] を receiver に送付。
  2. onBatchFlashLoan() を receiver に対して呼び出す(これは IERC3234BatchFlashBorrower に定義された関数)。
  3. onBatchFlashLoan() が返す値が keccak256("ERC3234BatchFlashBorrower.onBatchFlashLoan") と一致することを検証。
  4. 返済として amounts[i] + fees[i] を回収できなければ、トランザクションを revert。
  5. すべて正常に完了した場合、true を返す。
コールバックへの引数の条件
  • msg.sender を initiator として onBatchFlashLoan() に渡す必要があります。
  • tokens、amounts、data は書き換えせずにそのまま渡す必要があります。
  • fees[i] == flashFee(tokens[i], amounts[i]) となるように各手数料を計算して渡す必要があります。

Receiver

Receiver(借り手)コントラクトは、IERC3234BatchFlashBorrower インターフェースを実装する必要があります。

IERC3234BatchFlashBorrower.sol
pragma solidity ^0.7.0 || ^0.8.0;


interface IERC3234BatchFlashBorrower {

    /**
     * @dev Receive a flash loan.
     * @param initiator The initiator of the loan.
     * @param tokens The loan currency.
     * @param amounts The amount of tokens lent.
     * @param fees The additional amount of tokens to repay.
     * @param data Arbitrary data structure, intended to contain user-defined parameters.
     * @return The keccak256 hash of "ERC3234BatchFlashBorrower.onBatchFlashLoan"
     */
    function onBatchFlashLoan(
        address initiator,
        address[] calldata tokens,
        uint256[] calldata amounts,
        uint256[] calldata fees,
        bytes calldata data
    ) external returns (bytes32);
}

onBatchFlashLoan

js
function onBatchFlashLoan(
    address initiator,
    address[] calldata tokens,
    uint256[] calldata amounts,
    uint256[] calldata fees,
    bytes calldata data
) external returns (bytes32);
  • フラッシュローンを受け取る時に呼び出される関数。
  • initiator はローンを発行したアカウントです(通常はLenderの msg.sender)。
  • この関数の終了前に、各 tokens[i] に対して amounts[i] + fees[i] を msg.sender(Lender) に引き出し許可(approve)を行う必要があります。
  • 処理が正常に終了するには、必ず keccak256("ERC3234BatchFlashBorrower.onBatchFlashLoan") を返す必要があります。

補足

対応していないトークンの場合のエラー

対応していないトークンに対して数値を返すと誤解を招く可能性があるため、明示的に処理をrevertする仕様となっています。

関数名の選定理由

batchFlashLoan は、処理内容が明確に伝わり他の関数と衝突しにくい名前として選ばれました。 また、トークンが貸し手(Lender)によって保有または新規発行される両方のケースに対応できます。

receiver を関数の引数に含めることで、ローンの開始者と実際の受取先を分けて定義可能となり、柔軟な実装が可能です。

onBatchFlashLoan は、受信側で他の関数名と衝突しにくく、EIP667で使われている on〇〇 形式の命名規則にも合致します。

実装例との整合性

Aave・dYdX・Uniswap などの既存プロトコルでは、単一のコントラクトで複数トークンのフラッシュローンが提供されています。 ERC3234でも、関数引数にトークン配列(tokens)を含める設計にすることで、現実のプロトコルと整合性を持たせています。

任意データ(data)の活用

bytes calldata data は呼び出し元が独自のパラメータを渡すためのフィールドで、標準の汎用性を損なうことなく拡張性を持たせるために設けられています。

initiator(開始者)と amount の明示的な引渡し

initiator(ローンの開始者)はLenderから msg.sender として取得できるため、わざわざ data に埋め込むのではなく引数として渡す方が正確で安全です。

amounts も同様に、データに埋め込むと検証が複雑になるため、直接引数として渡す方がシンプルで信頼性があります。

fee(手数料)の引渡し設計

手数料(fees)も直接引数で渡すことで、受信側(Receiver)が返済に必要な正確な額を把握しやすくなります。

トークン返済の「pull」方式採用の理由

貸し手が transferFrom を使って借り手からトークンを引き出す「pull型」の設計が採用されています。 これにより、Lender側で他の機能との連携がしやすくなります。

一方、借り手からの「push型」でトークンを返す実装も可能ですが、その場合全ての機能をpushベースで作る必要があり複雑性が増します。

そのため、より一般的で扱いやすい「pull型」が選択されました。

セキュリティ

onBatchFlashLoan の引数検証

onBatchFlashLoan に渡される各引数はローンの条件を表していますが、そのまま信用することはできません。 引数は以下の2つのグループに分類できます。

トランザクション履歴に基づく引数

  • initiator(ローン開始者)
  • tokens(貸し出されたトークン)
  • amounts(貸し出された数量)
  • fees(手数料)

これらの値は、呼び出し元が正直でない場合には偽装される可能性があります。 信頼できる値として扱うには、onBatchFlashLoan の呼び出し元(コントラクトアドレス)が信頼済みのLenderリストに含まれている必要があります。

任意データ(data)

data の内容を信頼するには、上記の呼び出し元チェックに加えて、initiator が信頼できるアドレスのリストに含まれていることを確認する必要があります。

自動承認のリスク

借り手(Receiver)が onBatchFlashLoan の中で Lender に対して amount + fee の承認(approve)を行う場合には、LenderやInitiatorが信頼できることを前提としなければなりません。

もし、LenderがonBatchFlashLoanの返り値(keccak256ハッシュ)を正しく検証しない実装だった場合、悪意のある者が batchFlashLoan を呼び出し、被害者のアカウントから許可されたトークンを上限まで引き出すという攻撃が可能になります。

このようなリスクを防ぐためにも、Lenderは仕様どおりに onBatchFlashLoan の返り値を厳密に検証する必要があります。

フラッシュミントの外部的リスク

フラッシュミントにより一時的に大量のトークンが発行されるため、それに伴う新しい攻撃パターンに対して備える必要があります。

例1:金利操作攻撃

  • 1DAI をフラッシュミント
  • それを担保として1.5Mドル分のETHと一緒に預ける
  • 総預け入れ量が急増し、固定金利が 0.00001% に低下
  • 1M DAI をほぼ無利子で借りる
  • フラッシュミント分をBurnして返済
  • 実質的に永久に利子のないローンが成立

対策として、金利に下限・上限を設けること、また一時的な流動性変動に応じて再計算されるよう設計することが必要です。

例2:整数オーバーフロー・アンダーフロー

制限なしにフラッシュミントが可能な場合、2^256 - 1 といった極端な数値が使われる可能性があります。

こうした状況を安全に処理するためには、OpenZeppelinの SafeMath ライブラリなどで演算を保護することが推奨されます。

Solidityの0.8ではデフォルトでオーバーフロー・アンダーフロー対策がされています。

フラッシュミントの内部的リスク

プラットフォーム特有の機能とフラッシュミントが組み合わさると、想定外の動作が発生するリスクがあります。

例:トレジャリー資金の流出(Yield Protocol)

  • fyDAI を大量にフラッシュミント
  • 可能な限りDaiに交換(償還)する
  • jug.drip を呼び出して金利上昇をトリガー
  • 結果として Yield Protocol のCDPが担保不足に
  • MakerDAO上でCDPが清算され、プロトコルの資金が奪われる

このようなケースでは、内部機能とフラッシュローンの連携に注意し、常に悪用パターンを想定した検証を行うことが不可欠です。

引用

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

最後に

今回は「複数のトークンを一括で借りる仕組みを提案しているERC3234」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!