はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、DeFiにおけるフラッシュローンの仕組みを標準化し、異なるプロトコル間でも安全かつ効率的に一時的な資金の貸し借りを行えるようにする仕組みを提案しているERC7399についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。
概要
フラッシュローンとは、ブロックチェーン上で動作するスマートコントラクト(自動で契約内容を実行するプログラム)同士の間で行われる特殊なローン取引のことです。
この仕組みでは、**借り手(borrower)**は取引の最中に資金を借り、同じトランザクション(1つの処理のまとまり)内で借りた資金と手数料を返済する必要があります。
つまり、借入から返済までが1回の取引の中で完結します。
もし返済が完了しなければ、その取引全体が取り消される(ロールバックされる)仕組みになっています。
ERC7399では以下を定義しています。
- フラッシュローンを提供する**貸し手(lender)**がどのように貸出リクエストを受け入れるか
- 借り手(borrower)がどのように一時的にトランザクションの制御を得るか
- 安全にフラッシュローンを実行するための手順
これにより、異なるフラッシュローンサービス間でも同一のインターフェースを使って利用できるようになります。
動機
現在のフラッシュローンの仕組みは、サービス提供者ごとに異なる仕様で動作しています。
この非標準化(標準がない状態)が、開発者にとって以下のような課題を生んでいます。
-
統一された仕組みがないため開発工数が増えている
各プラットフォームが独自の実装を持つため、異なるサービスを利用する時に毎回異なるコード対応が必要になります。 -
エコシステム全体が拡大するほど複雑化が進む
フラッシュローンの利用が広がるにつれて、互換性を保つためのメンテナンスが増えて開発コストやリスクが上昇しています。
現在の課題の詳細
既存のフラッシュローン実装を分析すると、以下のような違いが見られます。
| 項目 | 実装の違い・課題点 |
|---|---|
| フラッシュローン開始時の構文 | 各プラットフォームで呼び出し方が異なり、統一されていない |
| 借り手とコールバック受信者の関係 | 一部では両者を同一アドレスに固定し、他では別々のアドレスを許可している |
| 返済方法 | 自動で貸し手が資金を引き取る方式と、借り手が手動で返す方式の2種類が存在する |
| フラッシュミンティング(Flash Minting) | 一部のプロトコルでは、ネイティブ資産を制限なく生成し手数料を取らない設計となっており、資産保有量ではなく計算資源に制約されるケースがある |
これらの違いは、開発者が複数のプロバイダーを利用しようとする際に大きな障害となっています。
提案の目的と利点
ERC7399の目的は、フラッシュローンに関する仕様を標準化し、より安全で柔軟な仕組みを提供することです。
標準化によって以下のような利点が期待されます。
- 借り手は異なるレンダー間でコードを書き換えることなくフラッシュローンを利用できるようになる。
- レンダー側も標準化された方法でリクエストを受け付けられるため、統合コストが大幅に削減される。
- エコシステム全体が互換性を持つことで、開発スピードとセキュリティが向上する。
仕様
フラッシュローンの基本構造
ERC7399で定義されるフラッシュローンとは、ERC20トークンを対象とする短期ローンです。
貸し手(Lender)から一定量の資産を借り、1回のフラッシュコール(flash関数の呼び出し)内で返済する必要があります。
フラッシュローンの流れ
| 段階 | 説明 |
|---|---|
① Initiator(呼び出し元)がflash()関数を呼び出す |
借り手、コールバック関数、資産の種類、金額などを指定します。 |
| ② Lenderが資産をLoan Receiver(借り手)に送る | 指定された金額のトークンが一時的に移動します。 |
| ③ LenderがCallback Receiverの関数を実行 | ここでローンに関連する処理(アービトラージ、清算など)を行います。 |
| ④ Callback内で返済が行われる | 借りた金額+手数料をPayment Receiverに返却する必要があります。 |
| ⑤ Callbackの結果を返す |
flash()の戻り値として、コールバック関数の返り値が返されます。 |
返済が完了しない場合、トランザクション全体が自動的に取り消されます(revert)。
なお、手数料はゼロ(0)でも問題ありません。
フラッシュローンの主要な構成要素
-
Lender(貸し手)
フラッシュローンを提供するコントラクト。どの資産を貸し出すかは自由に決定できます。 -
Initiator(呼び出し元)
フラッシュローンの実行を開始するアドレスです。 -
Loan Receiver(借り手)
実際に資金を受け取るアドレスです。 -
Callback Receiver
借りた資金を使って処理を行う関数を実行するアドレスです。 -
Payment Receiver(返済先)
ローンと手数料を受け取るアドレスです。
Lender(貸し手)仕様
Lenderは必ずERC7399インターフェースを実装する必要があります。
以下がそのインターフェース定義です。
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.6.0 <0.9.0;
import { IERC20 } from "./IERC20.sol";
interface IERC7399 {
/// @dev 貸し出し可能な最大金額を返す
function maxFlashLoan(address asset) external view returns (uint256);
/// @dev 指定したローンに対する手数料を返す
function flashFee(IERC20 asset, uint256 amount) external view returns (uint256);
/// @dev フラッシュローンを実行する
function flash(
address loanReceiver,
ERC20 asset,
uint256 amount,
bytes calldata data,
function(address, address, IERC20, uint256, uint256, bytes memory) external returns (bytes memory) callback
)
external
returns (bytes memory);
}
| 関数名 | 説明 | 実装上の要件 |
|---|---|---|
maxFlashLoan(asset) |
指定した資産で貸出可能な最大量を返す |
revert(失敗)してはいけない。貸出不可なら0を返す。 |
flashFee(asset, amount) |
指定資産・金額の手数料を返す |
revertしてはいけない。貸出不可能な場合はtype(uint256).maxを返す。 |
flash() |
実際にフラッシュローンを実行 | コールバックの実行や返済確認など、メインの処理を行う。 |
flash関数の実行内容
flash()関数が呼び出されると、以下の処理が行われます。
- 指定された資産と金額をLoan Receiverに送付する。
- その後、Callback Receiverの関数を実行する。
この関数呼び出しには、次のようなパラメータが渡されます。
bytes memory result = callback(
msg.sender, // Initiator
address(this), // Lenderコントラクト
asset, // 貸し出すトークン
amount, // 金額
_fee, // 手数料(flashFee関数の結果)
data // エンコードされたユーザーデータ
);
- Callback実行後、Payment Receiverのトークン残高が「初期残高 + 金額 + 手数料」になっていなければリバートする。
-
flash()の戻り値は、Callback関数の戻り値と同一でなければならない。
Receiver(コールバック受信側)仕様
フラッシュローンのコールバック関数を受け取る側(Receiver)は、以下の形式の外部関数を少なくとも1つ実装する必要があります。
/// @dev コールバック関数の標準形式
/// @param initiator フラッシュローンを開始したアドレス
/// @param paymentReceiver 返済を受け取るアドレス
/// @param asset 借りたトークン
/// @param amount 借りた金額
/// @param fee 支払う手数料
/// @param data 任意のユーザーデータ
/// @return result コールバックの戻り値(ABIエンコード済み)
function(address, address, IERC20, uint256, uint256, bytes memory)
external
returns (bytes memory)
callback;
この関数名は任意であり、オーバーロード(同名で異なる引数を持つ複数定義)も可能です。
ただし、戻り値の型は必ずbytes memoryでなければなりません。
安全性と互換性のための要点
- フラッシュローンは1トランザクション内で完結するため、返済が行われない場合は自動的に全体が無効化されます。
- Lenderは、どの資産をサポートするかを自由に設定できます。
- 手数料(Fee)はゼロでも問題ありません。
- Callbackで任意の処理を実装可能ですが、返済が成立しない場合は失敗します。
補足
maxFlashLoan と flashFee の設計意図
共通の考え方
maxFlashLoan と flashFee の両方の関数は、貸し手が特定のローンを提供できない場合でも、revert(処理失敗)を起こさずに数値を返すように設計されています。
このアプローチによって、複数の貸し手(Lender)を比較・ソートする際に、例外処理を気にせず安全に判定が行えます。
| 関数名 | 動作 | 貸出が不可能な場合の戻り値 |
|---|---|---|
maxFlashLoan(asset) |
貸し出し可能な最大額を返す | 0 |
flashFee(asset, amount) |
指定資産と金額に対する手数料を返す | type(uint256).max |
このような設計にすることで、外部から見たときに「ローンが可能か不可能か」を数値比較だけで判断でき、余計なエラーハンドリングが不要になります。
flash 関数名の選定理由
flashという関数名は、「短期間で完結する動作(瞬時の実行)」を意味する動詞であり、
フラッシュローンの特徴を端的に表しています。
また、他の関数名との衝突(命名の競合)が起きにくく、さらに資産の貸し出しだけでなく**ミンティング(新規発行)**を行うケースにも対応できる汎用的な名前です。
資産パラメータ(asset)の設計理由
現在、多くのフラッシュローンプロトコルでは、1つのコントラクトで複数のトークン資産を扱っています。
そのため、flash関数とコールバック関数の両方でassetパラメータを指定できるようにすることで、実際の運用に即した設計となっています。
この構成により、特定の資産ごとに異なる処理を行う時にも、明確かつ一貫した方法で対応できます。
data パラメータの意義
dataパラメータは、イニシエーター(呼び出し元)が任意の情報を受信側に渡すためのものです。
この値はABIエンコード(スマートコントラクト間でのデータ送信フォーマット)された任意のバイト列であり、コールバック関数側で自由にデコードして利用できます。
また、コールバック関数はbytes memory型の戻り値を持つため、受信側からイニシエーターへも任意のデータを返せます。
この双方向通信設計により、動的で柔軟なローン処理を実現しています。
initiator の扱い
多くの場合、コールバック関数内で「誰がローンを開始したか(initiator)」を知る必要があります。
この情報はmsg.sender(関数を呼び出したアドレス)としてLenderが把握しており、
コールバックの引数として直接渡す方式が採用されています。
代替案として「dataの中にinitiator情報を埋め込む」方法も考えられますが、その場合は受信側で正当性を検証する追加の仕組みが必要となり、
セキュリティ面でのリスクと実装の複雑化を招くため、非推奨とされています。
loanReceiver と callbackReceiver の分離
loanReceiver(ローン受取人)を明示的なパラメータとして定義している理由は、ローンの開始者・資金受取者・コールバック実行者を分離して設計できる柔軟性を確保するためです。
ただし、実際にはこれら3つが同一アドレスであるケースが多いため、loanReceiverはコールバック関数には直接渡されません。
必要であればdataパラメータに埋め込んで管理できます。
paymentReceiver の役割
paymentReceiver(返済受取人)は、返済フローを柔軟に構築するための設計要素です。
これにより、資産の移動(asset flow)と処理の流れ(control flow)を独立して扱えます。
例えば、ローンを実行するコントラクトと返済を受け取るコントラクトを別にすることも可能です。
これにより、返済管理・手数料分配などの複雑な仕組みを安全に実装できます。
amount と fee の取り扱い
amount(借入額)とfee(手数料)は、いずれもコールバック関数内で確実に参照できるよう、引数として直接渡されるようになっています。
これをdataの中に含める方法も考えられますが、
その場合、受信側で正確性を検証する仕組みが必要となり、安全性を損ねるため採用されていません。
また、feeはローンの返済計算や利益算出において重要な値であるため、独立した引数として明示的に渡す方が効率的で明快です。
コールバック関数の柔軟性
コールバック関数を「任意の外部関数」として定義できるようにすることで、フラッシュローンごとに異なる動作を実装可能にしています。
これにより、ローンの種類や戦略(例:アービトラージ、担保交換など)に応じて自由に挙動を定義できます。
関数呼び出し形式は24バイトで構成されており、最初の20バイトがコール先アドレス、残りの4バイトが関数シグネチャです。
この仕組みにより、異なる関数を明確に区別して安全に呼び出せます。
push型返済アーキテクチャの採用理由
返済は「プッシュ型(push architecture)」で行われる設計になっています。
これは、Lender(貸し手)がPayment Receiver(返済先)に資産が返却されたことを能動的に確認できる方式です。
「プル型(pull architecture)」では、貸し手が返済資産を引き出す形式になりますが、この場合、複数の処理が絡むとセキュリティ上のリスクが高まります。
一方、プッシュ型は返済の整合性と自動検証が容易であり、より安全な実装が可能です。
なお、もしLenderがプル型しか実装できない場合でも、ラッパーコントラクトを利用すれば、ERC7399に準拠した形で運用することができます。
互換性
ERC7399は、既存のERC3156(従来のフラッシュローン標準)の後継として提案されています。
ERC3156では、フラッシュローンの基本的な流れ(資産の貸出・返済・手数料支払い)が定義されていましたが、プロトコル間で細かな仕様の違いが生じやすく互換性の維持が課題でした。
ERC7399では、この問題を解決するために、より柔軟で拡張性の高い統一的なインターフェースを定義しています。
このため、ERC3156との直接的な互換性(同じコードでそのまま動くこと)はありません。
ERC3156については以下の記事を参考にしてください。
セキュリティ
コールバック引数の検証
フラッシュローンのコールバック関数で受け取る引数は常に正しいとは限りません。
貸し手(Lender)や呼び出し元(Initiator)を偽装される可能性があるため、すべての値を無条件に信用してはいけません*
引数の分類と検証の必要性
コールバックの引数は大きく2つのグループに分けられます。
| グループ | 検証が必要な理由 | 主な引数 |
|---|---|---|
| フラッシュローンの条件に関する引数 | 過去の取引内容を示すが、実際には偽装可能 |
initiator(呼び出し元)、asset(資産)、amount(金額)、fee(手数料) |
| 追加データ | 呼び出し元が任意に設定可能で、改ざんされる恐れがある |
data(任意のデータ) |
これらの値は、コールバックを呼び出した側が自由に設定できるため、意図的に誤った情報を渡すことが可能です。
そのため、コントラクト内で「本当に正しい値か」を検証する必要があります。
信頼できる引数とその検証方法
initiator、asset、amount、feeの値を信頼するためには、コールバックを呼び出したアドレスが「承認済みのフラッシュローン提供者(verified lender)」であることを確認するのが基本です。
多くのケースでは、flash()を実行した貸し手がそのままコールバックを行うため、「呼び出し元が既知のアドレスであること」を確認するだけで安全性を確保できます。
ただし、別の貸し手経由で呼び出される場合や、複数レイヤーを経由する構造では、ホワイトリストに登録された貸し手のみを信頼する実装が推奨されます。
data パラメータの検証
data引数は任意の情報を含められるためさらに慎重な扱いが必要です。
その内容を信用するには、前項の「貸し手確認」に加えて、initiator(トランザクション発行者)が信頼できるアドレスであることも確認する必要があります。
つまり、「貸し手と呼び出し元の両方を信頼できる場合」に限り、dataを安全に利用できます。
フラッシュローンにおけるセキュリティ考慮点
フラッシュローンは、取引の中で一時的に巨額の資産を動かすことができる強力な仕組みです。
その分、誤った設計や検証不足は即座に悪用されるリスクを伴います。
自動承認
返済処理を自動で行うコントラクトは、返済時のコールバック内で貸し手(Lender)および呼び出し元(Initiator)が信頼できる相手であるかを確認するメカニズムを必ず実装しなければなりません。
安全な方法としては以下のような設計が考えられます。
| 方法 | 内容 |
|---|---|
| 状態変数による追跡 | フラッシュローン開始時に「誰が、いくら借りたか、どんな手数料が発生するか」をコントラクト内に記録し、返済時に一致するかを確認する。 |
| 独自ロジックによる検証 |
loanReceiverに送金された金額を確認し、正しい手数料(fee)が支払われたかを独自の基準で判断。 |
| 信頼済みアドレス制御 | InitiatorやLenderをあらかじめ信頼リストに登録しておく。 |
これらの仕組みによって、悪意のある第三者が不正なローンを仕掛けるリスクを減らせます。
フラッシュミンティングに関する外部的セキュリティ考慮
フラッシュミンティングとは、資産の供給量を一時的に増やす仕組みです。
これは新しいタイプの攻撃リスクを生み出す可能性があります。
スポットオラクル操作
フラッシュミントで大量の資産を一時的に供給できるため、その資産の供給量を参照して価格を計算するオラクル(価格情報提供サービス)が誤作動する恐れがあります。
攻撃者はこれを利用して価格を不正に操作し、他のDeFiプロトコルで利益を得ることができます。
対策としては以下が挙げられます。
| 対策 | 内容 |
|---|---|
| フラッシュミント分を除外 | オラクルの供給データからフラッシュミント分を除外して計算する。 |
| 時間平均化 | 短時間の変動に影響されないよう、一定期間の平均値を使用する。 |
| その他の保護ロジック | 供給量の急変を検知して異常値を無視する。 |
算術オーバーフロー/アンダーフロー
フラッシュミントの実装で発行上限が設定されていない場合、攻撃者が任意の量のトークンを発行できる可能性があります。
この場合、数値の上限・下限(オーバーフロー・アンダーフロー)によって予期せぬバグや資産流出を招くおそれがあります。
そのため、受け取る側のプロトコルは次のような対策を取る必要があります。
| 対策 | 内容 |
|---|---|
| コンパイラによる自動保護 | オーバーフロー検知機能を持つコンパイラを使用する。 |
| 明示的なチェック | コントラクト内で「最大発行量」や「返済額の範囲」をコード上で検証する。 |
フラッシュミンティングに関する内部的セキュリティ考慮
フラッシュミントを既存の金融ロジック(担保管理・貸出・償還など)と同一のコントラクト内で扱うと、想定外の副作用や資金流出(Treasury Draining)を引き起こす可能性があります。
Treasury資金の流出の例
以下のような仕組みを持つスマートコントラクトを想定します。
- コントラクトが自社トークン(FOO)を貸し出す機能を持つ。
- 同時に、ユーザーがFOOをBurnした際に第三者から資金を借りる設計になっている。
- これにより、複数ユーザーの債務を1つの外部アカウントに集約する。
この構造では、フラッシュミントを悪用して以下のような攻撃が可能です。
| 攻撃ステップ | 説明 |
|---|---|
| ① | 貸し手から大量のFOOをフラッシュミントする。 |
| ② | FOOを別の資産BARに交換し、貸し手が第三者から借入上限まで資金を引き出す。 |
| ③ | その結果、基盤となる貸出プラットフォームの利率が上昇。 |
| ④ | 貸し手が過剰負債(アンダーコラテラル状態)となり、清算(liquidation)される。 |
| ⑤ | 攻撃者はこの清算差益を得る。 |
このように、内部ロジックとフラッシュミンティングが密結合している設計は非常に危険です。
安全な実装を行うためには、ミント機能と資金運用機能を明確に分離し、貸出と担保の関係を厳密に制御することが必要です。
引用
Alberto Cuesta Cañada (@alcueca), Michael Amadi (@AmadiMichaels), Devtooligan (@devtooligan), Ultrasecr.eth (@ultrasecreth), Sam Bacha (@sambacha), "ERC-7399: ⚡ Flash Loans ⚡ [DRAFT]," Ethereum Improvement Proposals, no. 7399, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7399.
最後に
今回は「DeFiにおけるフラッシュローンの仕組みを標準化し、異なるプロトコル間でも安全かつ効率的に一時的な資金の貸し借りを行えるようにする仕組みを提案しているERC7399」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!