Skip to content
On this page

L3-2: Lending App(レンディングアプリケーション)

1. 問題

DeFiレンディングプロトコルの中核的課題は、分散型かつKYCなしの環境でいかに安全に資金を貸し出すかということです。従来の銀行は信用スコアと担保保管に依存してリスクを管理しますが、スマートコントラクトはオフチェーンの信用評価を実行できません。そのため、DeFiレンディングでは過剰担保モデルを採用する必要があります。つまり、借り手は借入額よりも高い価値の担保を預け入れる必要があります。しかし、これにより次のような一連の設計課題が生じます。担保率はどのように決定するか、金利はどのように動的に調整するか、担保価値が下落した場合に預金者の資金をどのように保護するか、などです。

完全な分散型レンディングプロトコルは、上記のすべての問題を正確に解決すると同時に、コントラクトの論理的正しさと資金の安全性を確保する必要があります。AaveやCompoundなどの主要プロトコルはこのモデルの実現可能性を検証しており、合計で数千億ドルのTVLを管理しています。

2. 理由

レンディングはDeFiの3本柱(取引、レンディング、デリバティブ)の中で最もTVLの高い分野です。預金利息の発生を支えるシェアモデルから、健全性係数の計算、清算ロジックに至るまで、レンディングプロトコルの内部動作メカニズムを理解することは、DeFi開発者の中核的能力です。この知識は、レンディングプロトコルの評価や構築、レバレッジ戦略、清算ボットに直接応用できます。

過剰担保レンディングのユニークな点は、信頼不要の信用市場を創造することです。借り手は信用スコアではなく、オンチェーン担保の透明な価値に依存します。預金者は借り手のデフォルトリスクを引き受けることで利息を得ます(清算が発生した場合、預金者の資金は清算人が担保を購入することで保護されます)。この自己実行型の清算メカニズムこそ、DeFiレンディングを伝統的金融と区別する中核的イノベーションです。

3. 解決策

LendingApp.solは、預入、借入、返済、清算の4つのコア機能を含む簡略化されたレンディングプロトコルを実装しています。

預入メカニズム:ユーザーはdeposit()を呼び出してERC20トークンをコントラクトに預け入れます。預金は貸出のための流動性プールとして機能します。預け入れられたトークンの総量はtotalDepositedに記録されます。

借入メカニズム:ユーザーはborrow()を呼び出してトークンを借り入れます。借入前に健全性係数を確認する必要があります。これは担保価値と借入価値の比率です。コントラクトはCOLLATERAL_RATIO = 150%を使用しており、100ドルを借りるには150ドルの担保を預け入れる必要があることを意味します。健全性係数は_healthFactor()で計算されます:hf = (collateralValue * 100) / debtValue。ここでdebtValue = borrowed * borrowIndexです。

金利モデル:簡略化された線形金利アキュムレータを使用します。borrowIndex1e18(1.0)から始まり、時間とともに増加します。_accrueInterest()は各状態変更の前に呼び出され、経過時間に応じた利息を累積します。年率5%、秒単位で線形計算されます。借り手の実際の債務 = borrowed * borrowIndex / borrowerIndexAtBorrowTimeです。

清算メカニズム:健全性係数がCOLLATERAL_RATIO(150%)を下回った場合、誰でもliquidate()を呼び出して借り手の債務を返済し、担保に10%の清算ボーナスを加えて受け取ることができます。清算人が支払う債務額は借り手のborrowed総量に等しく、受け取る担保 = debtValue * (100 + 10) / 100で、預入トークンに換算されます。

参考ソースファイル:src/level3/LendingApp.sol

4. 遭遇した落とし穴

  • 利息累積の精度喪失_accrueInterest()で整数除算を使用しており、期間が短い場合にinterestFactorが0に切り捨てられる可能性があります
  • 清算時の数値オーバーフローcollateralToSeizeの計算にdebtValue * (RATIO_PRECISION + LIQUIDATION_BONUS) / RATIO_PRECISIONを使用していますが、debtValue自体が借入指数の膨張により大きくなっている可能性があります
  • 引出後の健全性係数チェックwithdraw()関数では引出前と引出後(仮定)の両方で健全性係数を検証する必要があり、チェック漏れがあると引出直後にポジションが不健全になります
  • 清算人からの二重徴収liquidate()では清算人が債務を支払うためにtransferFromが必要であり、同時にコントラクトが担保を返還するためにtransferも必要です。transferFromが失敗したのに担保状態が既に変更されていると、状態の不整合が発生します
  • 新規ユーザーの借入指数が未初期化:新規ユーザーが借入を行う際、borrowIndex[user]が0であるため、ゼロ除算エラーを回避するための特別な処理が必要です

5. 落とし穴の原因

利息累積の精度喪失は最も一般的な問題です。Solidityでは整数除算がゼロ方向に切り捨てられます。INTEREST_RATE * timeElapsedの値が365 days * 100より小さい場合、interestFactorの計算結果は0となり、金利がまったく累積されません。これは典型的なEVM精度問題です。Solidityには浮動小数点数がないため、すべての比率計算は除算の前に精度単位を拡大する必要があります。

LendingApp.sol_accrueInterest()における金利計算式は次のとおりです:

solidity
interestFactor = (INTEREST_RATE * timeElapsed * 1e18) / (365 days * 100);

timeElapsedが小さい場合(例:1ブロック時間の12秒)、5 * 12 * 1e18 / (31536000 * 100) = 非常に小さな数値です。この値が(1e18精度で)1未満の場合、除算結果は0となり、後続のborrowIndex = borrowIndex + (borrowIndex * 0) / 1e18も指数を変更しません。単一ブロックの丸めは無視できるように見えますが、累積効果により実際の金利が期待値を大幅に下回る可能性があります。

6. 落とし穴の解決方法

利息累積:短期間(数分から数時間)では、金利自体が非常に小さいため、0への切り捨ては許容範囲です。より高い精度が必要なシナリオでは、中間精度を上げる(例:1e27精度、つまりRAY単位の使用)か、指数累積式borrowIndex = borrowIndex * (1e18 + ratePerSecond * elapsed) / 1e18を使用して単一ステップの切り捨ての影響を軽減します。鍵となるのは、_accrueInterest()内でif (interestFactor > 0)をチェックし、実際に利息が発生した場合にのみ指数を更新することです。

清算計算:状態を変更する前に最終結果を計算します。ReentrancyGuard(コントラクトに継承済み)を使用してリエントランシー攻撃を防止します。liquidate()では、debtAmountcollateralSeizedtransferFromの前に計算され、pos.borrowedがトークン転送の前にゼロに設定されており、CEI(Checks-Effects-Interactions)パターンに準拠しています。

引出検証withdraw()では厳密な論理順序に従います。最初に_accrueInterest()、次に健全性係数の計算、その後に引出後の健全性係数が要件を満たすかの判定、最後に状態更新とトークン転送を実行します。remainingCollateral == 0 && pos.borrowed > 0の明示的チェックにより、債務があるのに担保がゼロになるケースを防止します。

7. 技術ポイント

技術ポイント説明
過剰担保率150%(COLLATERAL_RATIO)、$150を預けて$100を借入
健全性係数collateralValue * 100 / debtValue、150%未満で清算対象
清算ボーナス10%(LIQUIDATION_BONUS)、清算人は担保に10%のプレミアムを上乗せして受取
金利モデル簡略化された線形累積、年率5%、秒単位計算
borrowIndexグローバル借入指数、初期値1e18(18桁精度)
CEIパターンChecks-Effects-Interactions、外部呼び出しの前に状態更新
ReentrancyGuardすべての公開関数がnonReentrant修飾子を使用
単一トークンモデル預入と借入に同じERC20トークンを使用(簡略化)

Built with AiAda