1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[ERC7893] DeFiプロトコルの支払い能力をオンチェーンで証明・監視する仕組みを理解しよう!

1
Posted at

はじめに

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

今回は、DeFiプロトコルが自身の資産と負債をオンチェーンで公開し、支払い能力(ソルベンシー)をリアルタイムに検証・監視できる標準インターフェースを提案しているERC7893についてまとめていきます!

以下にまとめられているものを解説しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC7893とは、DeFiプロトコルがスマートコントラクト上で検証可能なソルベンシー証明(支払い能力の証明)を実装するための標準インターフェースです。

DeFiプロトコルにとって「自分たちが預かっている資産は、ユーザーへの負債をカバーできているのか」を透明に示すことは信頼の根幹です。
しかし、従来はプロトコルごとに報告方法がバラバラで、リアルタイムな財務健全性の把握が困難でした。
ERC7893は資産と負債の構造化されたデータ型を定義し、オラクルによる価格フィードと組み合わせて、ソルベンシー比率の計算、履歴データの保持、閾値を超えた場合のリスクアラートイベントの発行などを標準化します。
最低ソルベンシー比率として105%が設定可能で、プロトコルの財務状態を誰でもオンチェーンで照会できるようになります。

以下の図は、ERC7893全体のアーキテクチャを示しています。

ERC7893のアーキテクチャ概要

動機

この提案が生まれた背景には以下のような課題がありました。

財務報告の標準化の欠如

DeFiエコシステムでは、各プロトコルが独自の方法で財務健全性を報告しています。
あるプロトコルはダッシュボード上のオフチェーンデータで、別のプロトコルは独自のスマートコントラクトで資産状況を開示するなど、報告手法がバラバラです。
そのためプロトコル間の比較が困難で、投資家やユーザーがリスクを正確に評価できない状況が続いていました。

リアルタイム監視の不足

従来の方法では、プロトコルの財務状態は定期的な手動レポートに頼ることが多く、市場が急変した時に重大な変化を見逃す可能性がありました。
特に暗号資産の価格変動は非常に激しいため、数時間遅れのレポートではすでに手遅れになっているケースもあります。

早期警戒システムの不在

プロトコルの財務健全性が悪化しつつある場合でも、標準化された早期警戒の仕組みがありませんでした。
ユーザーや監視システムがプロトコルの危険な状態をタイムリーに検知できないと、Terra/LUNAの崩壊やFTXの破綻のような事態を未然に防ぐ手立てがなくなります。

監査プロセスの複雑さ

オンチェーンの監査を行う場合、プロトコルごとに異なるデータ構造や計算方法を理解する必要があり、過去のポジションを再構築するのに膨大な工数がかかっていました。
標準化されたインターフェースがあれば、監査ツールを共通化でき、効率が大幅に向上します。

クロスプロトコルリスク評価の困難さ

各プロトコルが独自の報告形式を採用しているため、複数プロトコルにまたがるシステミックリスクの評価が事実上不可能でした。
共通のインターフェースがあれば、複数プロトコルのリスクデータを集約し、今日の断片化された報告システムでは実現できないシステミックリスクの監視が可能になります。

この提案による解決策

ERC7893は、ISolvencyProofという共通インターフェースを定義することで上記のすべての課題に対処します。
資産と負債の構造を統一的に表現し、ソルベンシー比率を標準的な計算式で算出し、リスク閾値を超えた場合にイベントを発行する仕組みを提供します。
これにより、異なるプロトコルの財務健全性を同一の基準で比較・集約でき、システミックリスクの監視も可能になります。

オフチェーン報告やプロトコル固有の標準、より洗練された複雑なリスクモデルといった代替案も検討されましたが、オフチェーンでは検証可能性やトラストレス性が確保できず、プロトコル固有の標準では相互運用性の恩恵が得られず、複雑なリスクモデルは包括性と実装可能性のバランスを損なうと判断されたため、オンチェーンの共通インターフェースが最適なアプローチとして採用されました。

仕様

ERC7893に準拠する実装は、ISolvencyProofインターフェースを実装する必要があります。
また、ERC165supportsInterface関数を実装し、ISolvencyProofのインターフェースIDに対してtrueを返す必要があります。

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

コア要件

実装が満たすべき要件は以下です。

  • 資産と負債のトークンアドレス、数量、ETH建ての価値をコントラクトの状態変数に保存し、updateAssetsupdateLiabilities関数を通じて更新する
  • 各更新のタイムスタンプを記録する
  • ソルベンシー比率を (総資産 / 総負債) × 10000 の式で計算する
  • 指定した時間範囲のソルベンシー指標の履歴をクエリ可能にする(1年程度の保持期間を設定し、保持ポリシーを文書化する)
  • 財務指標が更新された時にSolvencyMetricsUpdatedイベントを発行する。リスク閾値を超えた時にRiskAlertイベントを発行することが推奨される
  • ProtocolAssetsProtocolLiabilities構造体内の配列はすべて同じ長さにする
  • すべての価格は18桁のETH建てで統一する

インターフェース定義

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

interface ISolvencyProof {
    struct ProtocolAssets {
        address[] tokens;
        uint256[] amounts;
        uint256[] values;
        uint256 timestamp;
    }

    struct ProtocolLiabilities {
        address[] tokens;
        uint256[] amounts;
        uint256[] values;
        uint256 timestamp;
    }

    event SolvencyMetricsUpdated(
        uint256 totalAssets,
        uint256 totalLiabilities,
        uint256 healthFactor,
        uint256 timestamp
    );

    event RiskAlert(
        string riskLevel,
        uint256 currentValue,
        uint256 threshold,
        uint256 timestamp
    );

    function getProtocolAssets() external view returns (ProtocolAssets memory);
    function getProtocolLiabilities() external view returns (ProtocolLiabilities memory);
    function getSolvencyRatio() external view returns (uint256);
    function verifySolvency() external view returns (bool isSolvent, uint256 healthFactor);
    function getSolvencyHistory(uint256 startTime, uint256 endTime)
        external view returns (
            uint256[] memory timestamps,
            uint256[] memory ratios,
            ProtocolAssets[] memory assets,
            ProtocolLiabilities[] memory liabilities
        );
    function updateAssets(
        address[] calldata tokens,
        uint256[] calldata amounts,
        uint256[] calldata values
    ) external;
    function updateLiabilities(
        address[] calldata tokens,
        uint256[] calldata amounts,
        uint256[] calldata values
    ) external;
}

このインターフェースは大きく分けて、データ構造(構造体)、イベント、クエリ関数(view関数)、更新関数の4つの要素で構成されています。
以下でそれぞれを詳しく説明します。

構造体

ProtocolAssets

struct ProtocolAssets {
    address[] tokens;
    uint256[] amounts;
    uint256[] values;
    uint256 timestamp;
}

プロトコルが保有している資産の状態を表す構造体です。
配列は並列構造になっており、tokens[i]amounts[i]values[i]がそれぞれ同じトークンの情報を表します。
例えば、プロトコルがWETHを100単位、USDCを50,000単位保有している場合、tokensにはそれぞれのトークンアドレスが、amountsにはトークン固有の精度での数量が、valuesにはETH建て(18桁精度)の換算価値が格納されます。

フィールド 説明
tokens address[] 追跡対象トークンのコントラクトアドレスの配列。
amounts uint256[] 各トークンの保有数量の配列。トークン固有のdecimalsで表現される。
values uint256[] 各トークンの保有量のETH建て価値の配列。18桁の精度で表現される。
timestamp uint256 最終更新のUnixタイムスタンプ(秒単位)。データの鮮度を検証するために使用される。

ProtocolLiabilities

struct ProtocolLiabilities {
    address[] tokens;
    uint256[] amounts;
    uint256[] values;
    uint256 timestamp;
}

プロトコルの負債(ユーザーへの返済義務など)の状態を表す構造体です。
構造はProtocolAssetsと同一で、レンディングプロトコルであれば借入残高、DEXであればLP保有者への返還義務などが負債として計上されます。

フィールド 説明
tokens address[] 負債に関連するトークンのコントラクトアドレスの配列。
amounts uint256[] 各負債の数量の配列。トークン固有のdecimalsで表現される。
values uint256[] 各負債のETH建て価値の配列。18桁の精度で表現される。
timestamp uint256 最終更新のUnixタイムスタンプ(秒単位)。

イベント

SolvencyMetricsUpdated

event SolvencyMetricsUpdated(
    uint256 totalAssets,
    uint256 totalLiabilities,
    uint256 healthFactor,
    uint256 timestamp
);

プロトコルの財務指標が更新された時に発行されるイベントです。
オフチェーンの監視システムやダッシュボードは、このイベントをリッスンすることでプロトコルの財務状態をリアルタイムに追跡できます。
updateAssetsまたはupdateLiabilitiesが正常に実行された後に必ず発行される必要があります。

パラメータ 説明
totalAssets uint256 資産価値の合計。ETH建て。
totalLiabilities uint256 負債価値の合計。ETH建て。
healthFactor uint256 ソルベンシー比率。(totalAssets / totalLiabilities) × 10000 で計算される。10500なら105%を意味する。
timestamp uint256 更新のUnixタイムスタンプ。

RiskAlert

event RiskAlert(
    string riskLevel,
    uint256 currentValue,
    uint256 threshold,
    uint256 timestamp
);

リスク閾値が超過した時に発行されるイベントです。
例えばソルベンシー比率が105%を下回った場合に"CRITICAL"レベルで発行され、監視システムや自動化ツールが即座に検知して対応アクションを取れるようになります。

パラメータ 説明
riskLevel string リスクの深刻度を示す文字列。"CRITICAL""HIGH_RISK""WARNING"のいずれか。
currentValue uint256 アラートをトリガーした現在の値。
threshold uint256 超過したリスク閾値。
timestamp uint256 アラートのUnixタイムスタンプ。

関数

getProtocolAssets

function getProtocolAssets() external view returns (ProtocolAssets memory);

プロトコルの現在の資産状態を取得する関数です。
外部の監視ツールやダッシュボード、他のスマートコントラクトがプロトコルの保有資産をリアルタイムに確認する時に呼び出します。
すべてのトークンアドレス、数量、ETH建ての価値、最終更新タイムスタンプを含むProtocolAssets構造体を返します。

戻り値

ProtocolAssets memory 現在の全資産状態を含む構造体。

getProtocolLiabilities

function getProtocolLiabilities() external view returns (ProtocolLiabilities memory);

プロトコルの現在の負債状態を取得する関数です。
getProtocolAssetsと対になる関数で、両方の結果を比較することでプロトコルの財務健全性を判断できます。

戻り値

ProtocolLiabilities memory 現在の全負債状態を含む構造体。

getSolvencyRatio

function getSolvencyRatio() external view returns (uint256);

現在のソルベンシー比率を計算して返す関数です。
計算式は (totalAssets * 10000) / totalLiabilities で、戻り値が10500であれば資産が負債の105%であることを意味します。
totalLiabilitiesが0の場合は、最大ソルベンシーを示す値を返すか、適切なエラーでリバートします。

例えば、総資産が100 ETH、総負債が80 ETHの場合、ソルベンシー比率は (100 * 10000) / 80 = 12500 となり、125%を意味します。

戻り値

uint256 ソルベンシー比率(10000倍にスケーリング済み)。

verifySolvency

function verifySolvency() external view returns (bool isSolvent, uint256 healthFactor);

プロトコルが支払い能力を維持しているかをブール値で返す関数です。
getSolvencyRatioがソルベンシー比率の生の数値を返すのに対し、この関数は最低要件を満たしているかの判定結果と、現在のヘルスファクターの両方を一度に返します。
DeFiプロトコル間の連携で、相手プロトコルの健全性を確認する時に便利です。

戻り値

bool isSolvent ソルベンシー比率が最低要件以上であればtrue
uint256 healthFactor 現在のソルベンシー比率。

getSolvencyHistory

function getSolvencyHistory(uint256 startTime, uint256 endTime)
    external view returns (
        uint256[] memory timestamps,
        uint256[] memory ratios,
        ProtocolAssets[] memory assets,
        ProtocolLiabilities[] memory liabilities
    );

指定した時間範囲内の過去のソルベンシー指標を返す関数です。
監査、リスクトレンド分析、規制報告などの用途で、過去のプロトコルの財務状態を遡って確認できます。
startTime以上endTime以下のタイムスタンプを持つすべての記録済みスナップショットが返されます。

ガス消費が大きくなる可能性があるため、実装では結果数に上限を設けることが推奨されています。

パラメータ 説明
startTime uint256 時間範囲の開始(Unixタイムスタンプ、秒)。
endTime uint256 時間範囲の終了(Unixタイムスタンプ、秒)。

戻り値

uint256[] memory timestamps 過去の更新タイムスタンプの配列。
uint256[] memory ratios 過去のソルベンシー比率の配列(10000倍スケーリング)。
ProtocolAssets[] memory assets 過去の資産状態の配列。
ProtocolLiabilities[] memory liabilities 過去の負債状態の配列。

updateAssets

function updateAssets(
    address[] calldata tokens,
    uint256[] calldata amounts,
    uint256[] calldata values
) external;

プロトコルの資産データを更新する関数です。
認可されたオラクルのみが呼び出すことができ、msg.senderが認可されたオラクルでない場合はリバートします。
tokensamountsvaluesの3つの配列は同じ長さでなければならず、異なる長さの場合もリバートします。
更新処理後にタイムスタンプがblock.timestampで自動設定され、SolvencyMetricsUpdatedイベントが発行されます。

パラメータ 説明
tokens address[] calldata 更新対象のトークンアドレスの配列。
amounts uint256[] calldata 各トークンの数量の配列。
values uint256[] calldata 各トークンのETH建て価値の配列。

updateLiabilities

function updateLiabilities(
    address[] calldata tokens,
    uint256[] calldata amounts,
    uint256[] calldata values
) external;

プロトコルの負債データを更新する関数です。
updateAssetsと同じバリデーションルールが適用され、認可オラクルのみが呼び出せます。
負債はポジション変更時のみ更新し、価格更新時には負債データを変更しないことが推奨されています。

パラメータ 説明
tokens address[] calldata 更新対象の負債トークンアドレスの配列。
amounts uint256[] calldata 各負債の数量の配列。
values uint256[] calldata 各負債のETH建て価値の配列。

オラクル管理

updateAssetsupdateLiabilitiesの呼び出しは認可されたアドレスに制限する必要があります。
認可されていないアドレスが呼び出した場合はリバートします。

オラクル管理関数の提供はオプションですが、提供する場合は認可変更時にイベントを発行することが推奨されています。

event OracleUpdated(address indexed oracle, bool authorized);
function setOracle(address oracle, bool authorized) external;

このパターンを実装することで、オラクルの追加・削除をオンチェーンで追跡できるようになります。
なお、オラクルの選択はプロトコルに委ねられており、Chainlink、API3、独自オラクルなど、どの価格フィードソリューションとも統合可能です。

補足

設計思想

ERC7893の設計は、信頼性、効率性、柔軟性、透明性の4つを優先しています。

データ構造の設計

ProtocolAssetsProtocolLiabilitiesの構造体では、トークン情報をマッピングではなく配列で管理しています。
配列ベースの設計を選んだ理由は3つあります。
まず、監視システムが一度の呼び出しで全データを取得できるため、状態の取得効率が高くなります。
次に、履歴追跡の要件との相性が良く、各時点のスナップショットをそのまま保存できます。
さらに、ボラティリティの高い市場環境でバッチ更新を行う時にも、シンプルな操作で済みます。

また、各トークンの数量(amount)と価値(value)の両方を記録する設計には、市場の急変時の耐性向上と、時系列での価値/数量比率の比較によるオラクル操作の検出、そして事後分析のための明確な監査証跡の確保という目的があります。

タイムスタンプを構造体の中に直接埋め込む設計にすることで、データの更新がアトミックに行われ、価格変動中に部分的な更新が残るリスクを防いでいます。

ソルベンシー計算の数学モデル

コアとなるソルベンシー比率(SR)は以下の式で計算されます。

$$SR = (TA / TL) \times 100$$

ここで、$TA$(Total Assets)は全資産価値の合計、$TL$(Total Liabilities)は全負債価値の合計です。
この数学的表現では105が105%を意味します。
スマートコントラクトの実装では、整数演算で小数点以下2桁の精度を確保するために × 10000 でスケーリングした (totalAssets * 10000) / totalLiabilities が使用されます。この場合10500が105%を意味します。両者は同じ比率を異なるスケールで表現したものです。
それぞれ $TA = \sum(A_i \times P_i)$、$TL = \sum(L_i \times P_i)$ として計算されます。
$A_i$は資産iの数量、$L_i$は負債iの数量、$P_i$は資産iまたは負債iの価格です。

さらに高度なリスク評価のために、リスク調整済みヘルスファクター(HF)も定義されています。

$$HF = \frac{\sum(A_i \times P_i \times W_i)}{\sum(L_i \times P_i \times R_i)}$$

$W_i$は資産iのリスクウェイト($0 < W_i \leq 1$)で、例えばステーブルコインは1.0に近い値、ボラティリティの高いトークンは0.5のように低い値を設定します。
$R_i$は負債iのリスクファクター($R_i \geq 1$)で、返済期限が近い負債ほど高い値が設定されます。

リスク閾値

テスト検証を経て、以下のリスク閾値が推奨されています。

リスクレベル ソルベンシー比率 求められる対応
CRITICAL 105%未満 緊急停止
HIGH RISK 105%〜110% リスクアラート
WARNING 110%〜120% 監視強化
HEALTHY 120%以上 通常運用

105%がクリティカル閾値として設定されている理由は、市場暴落シミュレーション(ETH価格80%下落、BTC価格70%下落)のテストにより、この水準を下回ると回復が困難になることが確認されたためです。
110%や120%はそれぞれ高リスク・警告レベルとして、段階的な早期警戒を可能にしています。

以下の図は、各リスク閾値の関係を示しています。

リスク閾値の関係図

以下の図は、閾値レベルの詳細な構成を示しています。

閾値レベルの構成図

流動性カバレッジ比率とシステムヘルスインデックス

コアのソルベンシー比率に加えて、流動性カバレッジ比率(LCR)とシステムヘルスインデックス(SI)も定義されています。

LCRは以下の式で計算されます。

$$LCR = \frac{HQLA}{TNCO} \times 100$$

$HQLA$は高品質流動資産(High Quality Liquid Assets)、$TNCO$は30日間の純現金流出額(Total Net Cash Outflows)です。
この指標により、プロトコルが短期の流動性ストレスに耐えられるかを評価できます。

システムヘルスインデックス(SI)は複数の指標を加重平均して総合的な健全性を評価します。

$$SI = \frac{SR \times w_1 + LCR \times w_2 + (1/\sigma) \times w_3}{w_1 + w_2 + w_3}$$

$w_1, w_2, w_3$は重み付け係数、$\sigma$はシステムのボラティリティです。
ソルベンシー比率、流動性、安定性の3つの観点からプロトコルの健全性を総合的にスコアリングします。

VaRとデフォルト確率

ERC7893のリスク評価フレームワークでは、Value at Risk(VaR)やデフォルト確率といった追加の指標も定義されています。

VaRは $VaR(\alpha) = \mu - (\sigma \times z(\alpha))$ で計算され、特定の信頼水準$\alpha$での最大損失額を推定します。
$\mu$は期待リターン、$\sigma$は標準偏差、$z(\alpha)$は信頼水準に対応するz値です。

デフォルト確率(PD)は $PD = N(-DD)$ で計算されます。
$DD$(Distance to Default)は $DD = \frac{\ln(TA/TL) + (\mu - \sigma^2/2)T}{\sigma\sqrt{T}}$ で求められ、Tは時間軸、$N()$は標準正規分布です。
この指標により、プロトコルが債務不履行に陥る確率を定量的に評価できます。

オラクル統合パターン

この規格は意図的にオラクル実装を柔軟に設計しています。
プロトコルは以下のような方法で価格フィードを統合できます。

  • Chainlink、API3などの既存オラクルネットワークとの直接統合
  • 複数オラクルソースからの価格集約
  • Uniswap V3のTWAP(Time-Weighted Average Price)の活用
  • 中央値ベースの価格フィード

以下の図は、オラクル統合のパターンを示しています。

オラクル統合パターン

実装上の推奨事項

テスト結果に基づく推奨事項として、負債は価格更新時にはそのまま維持し、実際のポジション変更がある場合にのみ更新することが挙げられています。
また、負債がゼロにならないようバリデーションを行うことで、ゼロ除算を防止できます。

ソルベンシー比率の計算では固定小数点演算を使用し、一貫した結果を得ることが推奨されています。

function calculateRatio(uint256 assets, uint256 liabilities) pure returns (uint256) {
    if (liabilities == 0) {
        return assets > 0 ? RATIO_DECIMALS * 2 : RATIO_DECIMALS;
    }
    return (assets * RATIO_DECIMALS) / liabilities;
}

このコードでは、負債がゼロの場合に資産があれば最大ソルベンシー(RATIO_DECIMALS * 2、つまり200%)を返し、資産もゼロの場合は100%を返します。
通常時は(assets * 10000) / liabilitiesで計算します。

ガスの最適化としては、履歴ストレージの効率化、複数トークンのバッチ更新、更新時の配列サイズ制限が推奨されています。

参考実装

参考実装では、本番環境を想定した包括的なセキュリティ機能が含まれています。

セキュリティ定数

uint256 private constant RATIO_DECIMALS = 10000;
uint256 private constant MIN_SOLVENCY_RATIO = 10500;    // 105%
uint256 private constant CRITICAL_RATIO = 10200;         // 102%
uint256 private constant WARNING_RATIO = 11000;          // 110%

uint256 private constant MAX_PRICE_DEVIATION = 500;      // 5%
uint256 private constant MAX_TOKENS_PER_UPDATE = 50;
uint256 private constant STALENESS_THRESHOLD = 3600;     // 1時間
uint256 private constant CIRCUIT_BREAKER_THRESHOLD = 2000; // 20%
uint256 private constant UPDATE_COOLDOWN = 5;            // 5ブロック
uint256 private constant MAX_HISTORY_ENTRIES = 8760;     // 約1年分
uint256 private constant MIN_ENTRY_INTERVAL = 3600;      // 1時間

bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

各定数の役割を説明します。
RATIO_DECIMALSはソルベンシー比率計算のスケーリング係数で、整数演算での精度を確保します。
MIN_SOLVENCY_RATIO(10500 = 105%)は支払い能力の最低ラインで、CRITICAL_RATIO(10200 = 102%)はさらに深刻な清算閾値です。

MAX_PRICE_DEVIATION(500 = 5%)はオラクル間の価格乖離の許容範囲で、これを超える更新は操作の疑いがあるため拒否されます。
MAX_TOKENS_PER_UPDATE(50)は1回の更新で処理できるトークン数の上限で、DoS攻撃を防止します。
STALENESS_THRESHOLD(3600秒 = 1時間)は価格データの有効期限で、古いデータに基づく計算を防ぎます。

CIRCUIT_BREAKER_THRESHOLD(2000 = 20%)は、資産価値が1ブロック内で20%以上変動した場合にシステムを自動停止するためのサーキットブレーカー閾値です。
UPDATE_COOLDOWN(5ブロック)は同一オラクルからの連続更新を制限し、スパムを防止します。

アクセス制御

modifier onlyOracle() {
    require(
        assetOracles[msg.sender] || hasRole(ORACLE_ROLE, msg.sender),
        "Not authorized oracle"
    );
    require(!emergencyPaused || block.timestamp > pauseEndTime,
        "Emergency paused");
    _;
}

modifier rateLimited() {
    require(
        block.number >= lastUpdateBlock[msg.sender] + UPDATE_COOLDOWN,
        "Update too frequent"
    );
    lastUpdateBlock[msg.sender] = block.number;
    emit RateLimitTriggered(msg.sender, block.number);
    _;
}

onlyOracle修飾子は、レガシーなオラクル管理(assetOraclesマッピング)と新しいロールベースのアクセス制御(hasRole)の両方に対応しています。
緊急一時停止中はオラクルの更新もブロックされますが、一時停止期間が終了すれば自動的に復帰します。

rateLimited修飾子は、同一オラクルが5ブロック(約1分)以内に連続更新することを防止し、スパム攻撃やガス消耗攻撃から保護します。

サーキットブレーカー

function _checkCircuitBreaker(uint256 previousTotal, uint256 newTotal) internal {
    if (previousTotal > 0) {
        uint256 assetChange = newTotal > previousTotal
            ? ((newTotal - previousTotal) * 10000) / previousTotal
            : ((previousTotal - newTotal) * 10000) / previousTotal;

        if (assetChange > CIRCUIT_BREAKER_THRESHOLD) {
            emergencyPaused = true;
            pauseEndTime = block.timestamp + 3600;
            emit CircuitBreakerTriggered("Large asset change",
                assetChange, CIRCUIT_BREAKER_THRESHOLD);
            emit EmergencyPaused(address(this), pauseEndTime);
        }
    }
}

資産価値が前回の更新から20%以上変動した場合、サーキットブレーカーが作動してシステムを1時間自動停止します。
これは、オラクル操作攻撃や異常な市場変動からプロトコルを保護するための仕組みです。
NYSE(ニューヨーク証券取引所)のサーキットブレーカー基準を参考にしています。

マルチオラクルコンセンサス

function _validatePriceConsensus(address token, uint256 proposedPrice)
    internal returns (bool) {
    address[] memory activeOracles = _getActiveOracles();
    if (activeOracles.length < 3) return true;

    uint256[] memory prices = new uint256[](activeOracles.length);
    uint256 validPrices = 0;

    for (uint256 i = 0; i < activeOracles.length; i++) {
        if (oraclePrices[activeOracles[i]][token] > 0) {
            prices[validPrices] = oraclePrices[activeOracles[i]][token];
            validPrices++;
        }
    }

    if (validPrices < 2) return true;

    uint256 median = _calculateMedian(prices, validPrices);
    uint256 deviation = proposedPrice > median
        ? ((proposedPrice - median) * 10000) / median
        : ((median - proposedPrice) * 10000) / median;

    if (deviation > MAX_PRICE_DEVIATION) {
        emit PriceDeviationAlert(token, deviation, activeOracles);
        return false;
    }

    return true;
}

3つ以上のアクティブなオラクルがある場合、提案された価格を中央値と比較し、5%以上の乖離があれば更新を拒否します。
これにより、単一のオラクルが不正な価格データを注入する攻撃を防止できます。
オラクルが3つ未満、または有効な価格データが2つ未満の場合は、検証をスキップして更新を許可します。

履歴データ管理

function getSolvencyHistory(uint256 startTime, uint256 endTime)
    external view returns (uint256[] memory, uint256[] memory,
        ProtocolAssets[] memory, ProtocolLiabilities[] memory) {

    uint256 count = 0;
    for (uint256 i = 0; i < metricsHistory.length; i++) {
        if (metricsHistory[i].timestamp >= startTime &&
            metricsHistory[i].timestamp <= endTime) {
            count++;
            if (count >= 100) break;
        }
    }

    uint256[] memory timestamps = new uint256[](count);
    uint256[] memory ratios = new uint256[](count);
    ProtocolAssets[] memory assets = new ProtocolAssets[](count);
    ProtocolLiabilities[] memory liabilities = new ProtocolLiabilities[](count);

    uint256 index = 0;
    for (uint256 i = 0; i < metricsHistory.length && index < count; i++) {
        if (metricsHistory[i].timestamp >= startTime &&
            metricsHistory[i].timestamp <= endTime) {
            timestamps[index] = metricsHistory[i].timestamp;
            ratios[index] = metricsHistory[i].solvencyRatio;
            assets[index] = metricsHistory[i].assets;
            liabilities[index] = metricsHistory[i].liabilities;
            index++;
        }
    }

    return (timestamps, ratios, assets, liabilities);
}

履歴データの取得は2パス方式で実装されています。
最初のパスで該当する件数をカウントし、正確なサイズの配列を確保してから、2回目のパスでデータを埋めていきます。
ガス上限を超えないように1クエリあたり最大100件の制限が設けられており、最大8760件(約1年分の時間単位記録)まで保持されます。

緊急対応システム

function emergencyPause() external onlyEmergencyGuardian {
    emergencyPaused = true;
    pauseEndTime = block.timestamp + 4 * 3600;
    emit EmergencyPaused(msg.sender, pauseEndTime);
}

function emergencyUnpause() external onlyEmergencyGuardian {
    emergencyPaused = false;
    pauseEndTime = 0;
    emit EmergencyUnpaused(msg.sender);
}

緊急ガーディアンが手動でシステムを一時停止できる仕組みも用意されています。
emergencyPauseはデフォルトで4時間の一時停止を行い、emergencyUnpauseで即座に解除できます。
サーキットブレーカーによる自動停止(1時間)とは異なり、人間の判断による停止は4時間とより長い期間が設定されています。

清算システム

参考実装ではオプションの清算システムも定義されています。

struct LiquidationConfig {
    uint256 maxLiquidationRatio;
    uint256 liquidationBonus;
    uint256 minHealthFactor;
    uint256 maxSlippage;
    bool isActive;
}
フィールド 説明
maxLiquidationRatio 1回のトランザクションで清算可能な負債の最大割合。
liquidationBonus 清算を実行する清算人への報酬(ボーナス)。
minHealthFactor 清算がトリガーされる最低ヘルスファクター。
maxSlippage 清算時に許容される最大スリッページ。
isActive 清算システムが有効かどうかのフラグ。

この清算システムはコアのソルベンシー監視とは分離されており、プロトコルが必要に応じて実装する拡張機能です。

互換性

ERC7893は既存のDeFiプロトコルと互換性があり、既存のトークン標準に変更を加える必要はありません。
ERC20トークンとの統合が前提とされており、資産・負債のトークンアドレスとしてERC20準拠のトークンが使用されます。

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

セキュリティ

オラクルセキュリティ

価格フィードの安全性はソルベンシー計算の根幹です。
単一のオラクルに依存すると、そのオラクルが侵害された場合にプロトコル全体が危険にさらされます。
最低3つの独立したオラクルソースを使用し、中央値で集約することが推奨されています。
ソース間の価格乖離が5%を超える場合は更新を拒否し、価格データが1時間以上古い場合は陳腐化として処理する必要があります。

Aave V3のAaveOracleやChainlinkのFeed Registryが実用的な統合パターンとして参考になります。

TWAP(Time-Weighted Average Price)を使用する場合は、操作耐性のために最低30分のウィンドウと、参照プールに最低100万〜500万ドルの流動性があることが推奨されています。

アクセス制御

パラメータ変更には3/5マルチシグ、重要なアップグレードには4/7マルチシグが推奨されています。
緊急一時停止の権限はメインガバナンスとは分離し、AaveのGuardianモデルを参考にした設計が有効です。

一時停止の発動間隔には最低1時間のクールダウンが設けられ、乱用を防止します。

リスク管理パラメータ

以下の表は、本番環境で検証済みのセキュリティパラメータをまとめたものです。

パラメータ 参考プロトコル
クリティカル比率 102% Aave V3のWBTC清算閾値
最低ソルベンシー比率 105% Compound V3のClose Factorトリガー
警告比率 110% MakerDAOの緊急シャットダウン閾値
価格乖離許容範囲 5% Chainlinkの偏差基準
陳腐化閾値 1時間 ChainlinkのETH/USDハートビート
サーキットブレーカー 20% NYSE基準
レート制限 5ブロック DoS防止
最大トークン数 50/更新 30Mガスブロック制限考慮

サーキットブレーカーと緊急対応

自動サーキットブレーカーのトリガー条件は、資産価値の変動が20%を超えた場合と、1時間以内に流動性が50%以上減少した場合です。
初期停止期間は1〜4時間で、繰り返しトリガーされた場合は指数バックオフが適用されます。
復帰は段階的に行われ、25%、50%、75%、100%と容量を段階的に戻しながら、各段階で30分間の監視を行います。

重要な操作にはタイムディレイが設定されています。
プロトコルのアップグレードは7日間(604,800秒)、閾値パラメータの変更は48時間(172,800秒)、オラクル権限の変更は24時間(86,400秒)の遅延が推奨されていますが、緊急時には即座にオーバーライドが可能です。

DoS攻撃防止

1回の更新あたり最大50トークン、履歴クエリあたり最大100件のページネーション、空配列の拒否と配列長の整合性チェック、OpenZeppelinのReentrancyGuardによるリエントランシー防止が推奨されています。

清算保護パターン

清算の統合では、ヘルスファクター120%以上が健全な状態で、110%〜120%でWARNINGアラート、105%〜110%でHIGH RISKアラートを経て、105%未満でCRITICAL(緊急停止・清算トリガー)という段階的なアプローチが推奨されています。
1回の清算で処理できる負債は最大50%に制限し、自動清算のスリッページ許容範囲は最大3%に設定することで、清算時の市場への影響を最小限に抑えます。

引用

最後に

今回は「ERC7893によるDeFiプロトコルのソルベンシー証明の仕組み」についてまとめてきました!
いかがだったでしょうか?

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

Twitter @cardene777

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?