2026年2月15日、MoonwellのcbETHオラクルが価格として$1.12を報告し始めました。実際の市場価格は$2,200でした。
4分以内に、清算ボットが健全なポジションから1,096 cbETHを押収しました。悪意ある借り手は数ペニーの担保を投入し、数千ドルを借り入れました。チームが対応できるまでに、$1.78百万(178万ドル)の不良債権が蓄積してしまい、さらにガバナンスのタイムロックがそれを許さないため、オラクルを修正できない状態が5日間続きました。
決定打:セキュリティ研究者Pashovは、影響を受けたコントラクトがAIモデルによって共同執筆されていたと指摘しました。「バイブ・コーディング(雰囲気でコーディング)」の時代が、DeFiの最初の被害者を出したのです。
MIP-X43の解剖
Moonwellのガバナンス提案MIP-X43は、通常のものになるはずでした。BaseとOptimism上のMoonwellの中核市場すべてに、Chainlink OEV(Oracle Extractable Value)ラッパーコントラクトを有効化するものでした。これは、そうでなければMEVのサーチャーに漏れてしまうオラクル更新から価値を取り込むことを目的としたアップグレードです。
この提案は複数のオラクル設定に触れていました。そのうちの1つ、cbETHには致命的なエラーが含まれていました。
本来起きるはずだったこと
cbETHのUSD価格は、2つのオラクルフィードを組み合わせて算出する必要があります。
cbETH_USD = cbETH_ETH × ETH_USD
cbETH/ETHフィードは取引所のレートを示します(ステーキング報酬が積み上がった結果、約1.12 ETHが1 cbETHに相当)。ETH/USDフィードはイーサのドル価格を示します(約$2,000)。両者を掛け合わせると、cbETH ≈ $2,240です。
実際に起きたこと
オラクルはcbETH/ETHフィードのみを使うよう設定されていました。つまり、ETH/USDを掛け合わせる手順なしの、生データのままです。
// WRONG: ETHでの交換レートを返す。USDではない
// cbETHオラクルは: 1.12(つまり1.12 ETH)
// プロトコルはこれをこう解釈する: $1.12
// CORRECT implementation:
// cbETH_price = cbETH_ETH_feed.latestAnswer() * ETH_USD_feed.latestAnswer()
// cbETH_price = 1.12 * 2000 = $2,240
1.12という数値自体は技術的に正しいものでした。実際にcbETH/ETHのレートだったからです。しかしプロトコルはそれを$1.12として解釈し、1.12 ETHとしては解釈しませんでした。2,000倍の価格誤り。
4分間のカスケード
T+0(18:01 UTC): MIP-X43が実行されます。オラクルがcbETHを$1.12として報告し始めます。
T+1〜2分: 自動清算ボットが、cbETH担保ポジションがすべて「不足担保」(約99.95%)になったことを検知します。清算が開始されます。ボットは、押収したcbETHあたり債務を約$1返済し、代わりに実際には$2,200相当の価値を得ることになります。
T+2〜3分: 日和見的な借り手は、最小限の担保でcbETHを$1.12で借りられることに気づきます。誰かが$100を投入し、80+ cbETH(実際の価値で$176,000)を借り入れます。
T+4(18:05 UTC): Moonwellのモニタリングが異常を検知します。
T+8分: チームとリスクマネージャーのAnthias LabsがcbETHの供給/借入上限を0.01に引き下げ、新規の活動を停止します。
しかし清算は続きました。 オラクルがまだ間違っていたため、既存のポジションはプロトコルの見方では「不足担保」のままでした。そしてオラクルを修正するには5日間のタイムロック付きのガバナンス投票が必要でした。
最終的な被害:$1.78M(178万ドル)の不良債権、清算人が1ドル未満の値段で買い取るような形で押収した1,096.317 cbETH。
AIの観点:「バイブ・コーディング」がDeFiにもたらすもの
Pashovが、MoonwellのコミットがClaudeによって共同執筆されたと指摘したとき、暗号セキュリティのコミュニティは沸き立ちました。「AIがバグを書いた」というのが見出しになりました。しかし現実はもう少し複雑で、なおさら憂慮すべき点があります。
AIがたぶんやったこと
MIP-X43のオラクル設定には、複数のアセットについてアドレス、フィードID、そして構成(コンポジション)ロジックを指定することが含まれます。これは、開発者がAIに生成させる典型的なボイラープレートそのものです。
// 「ChainlinkのcbETH/ETHフィードを使ってcbETHオラクルをセットアップする」
// AIが生成:
OracleConfig memory cbethConfig = OracleConfig({
asset: CBETH_ADDRESS,
feed: CHAINLINK_CBETH_ETH_FEED,
// Missing: secondaryFeed: CHAINLINK_ETH_USD_FEED,
// Missing: compositionType: MULTIPLY
});
プロンプトはたぶん正しかったはずです。生成されたコードも、おそらく文法的には正しかったでしょう。しかしcbETHには、単一フィードではなく合成(composed)されたオラクルが必要だという意味論(セマンティック)の要件が抜けていました。AIは、1.12が「ドル価格」ではなく「交換レート」だということを理解していないのです。
なぜAIがこのバグを起こしたのではないのか—テストが欠けていた
厳しい真実を言います:人間の開発者でも、同じ間違いをしていた可能性があります。オラクルの合成は、よく知られた落とし穴(フットガン)です。Chainlinkのドキュメントもそれを明確に警告しています。実際に起きた失敗は下流(ダウンストリーム)側でした:
エンドツーエンドの統合テストがなかったため、MIP-X43実行後にオラクル価格が市場の現実と一致していることを検証できませんでした。このような単一のテストでも防げたはずです。
// test/oracle-sanity.test.js
it("cbETHオラクルの価格が市場価格の5%以内に収まる", async () => {
const oraclePrice = await moonwell.getUnderlyingPrice(cbETH.address);
const marketPrice = await chainlinkAggregator.latestRoundData(); // ETH/USD × cbETH/ETH
const deviation = Math.abs(oraclePrice - marketPrice) / marketPrice;
expect(deviation).to.be.lessThan(0.05); // 5%の許容差
});
ガバナンスのシミュレーションがなかったため、MIP-X43が実行前にメインネットのフォークで検証されませんでした。TenderlyやFoundryのfork-urlのようなツールを使えば、これは簡単です。
forge test --fork-url $BASE_RPC --fork-block-number $PRE_MIP_BLOCK
プロトコル側に価格の健全性(サニティ)サーキットブレーカーがなかったのです。もしオラクルが突然99.95%もの価格下落を報告したなら、「たぶんすぐに全てを清算しないほうがいいのでは?」という話です。
本当の「バイブ・コーディング」リスク
危険なのはAIがバグを書くことではありません。人間がオラクルのバグを書いてきたのは何年も前からです。危険なのは、AIが、十分にレビューしていないコードに対して開発者を自信満々にさせてしまうことです。
手作業でオラクル設定を書くときは、頭の中で価格導出をモデリングしています。AIに「cbETHオラクルを設定して」と指示すると、その頭の中のモデリングを完全にスキップしてしまうかもしれません。コードはコンパイルされ、型チェックも通り、そこで先に進みます。
「雰囲気でコーディング(vibe coding)」はAIの品質の話ではありません。人間の注意力の話です。
Moonwellの最初のオラクル事故ではなかった
2025年11月、Moonwellは同様のオラクル関連の問題を経験しました。4か月の間に2件のオラクル誤設定があったことは、システム的な問題を示唆しています:
- オラクルのテストフレームワークがない — 各デプロイは手動による検証に依存している
- 自動の価格サニティチェックがない — デプロイ時でも実行時でも行われない
- 統治(ガバナンス)は緊急事態に対応できない — 悪意ある提案から守るための5日間のティムロックが、緊急の修正も妨げている
このパターンはDeFi全体で見られます。攻撃者がプロトコルから資金を引き出すことを防いだのと同じガバナンスティムロックが、チームがオラクルを直すことも妨げました。1つの脅威モデル(悪意あるガバナンス)向けに設計されたセキュリティメカニズムは、別の脅威モデル(設定ミス)による被害を拡大させてしまいます。
ディフェンスのパターン:実際に機能するオラクル・セキュリティ
1. マルチソースによる価格バリデーション
相互検証なしで単一のオラクルフィードに頼らないでください:
function getPrice(address asset) external view returns (uint256) {
uint256 primaryPrice = primaryOracle.getPrice(asset);
uint256 secondaryPrice = secondaryOracle.getPrice(asset);
uint256 deviation = _abs(primaryPrice, secondaryPrice) * 10000 / primaryPrice;
require(deviation < MAX_DEVIATION_BPS, "Oracle price divergence");
return primaryPrice;
}
2. 歴史的な価格サーキットブレーカー
function validatePriceUpdate(address asset, uint256 newPrice) internal view {
uint256 lastPrice = lastKnownPrice[asset];
if (lastPrice > 0) {
uint256 changePercent = _abs(newPrice, lastPrice) * 100 / lastPrice;
require(
changePercent < MAX_PRICE_CHANGE_PERCENT,
"Price change exceeds circuit breaker threshold"
);
}
}
価格が99.95%下落した場合、そのまま絶対に少なくとも一時停止(pause)なしで清算ロジックに流してはいけません。
3. ガバナンス・シミュレーションのパイプライン
# .github/workflows/governance-simulation.yml
name: Simulate Governance Proposal
on:
pull_request:
paths: ['proposals/**']
jobs:
simulate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fork mainnet and execute proposal
run: |
forge script SimulateProposal \
--fork-url $BASE_RPC \
--fork-block-number $(cast block-number --rpc-url $BASE_RPC)
- name: Verify oracle sanity post-execution
run: |
<ここにフォージテストが入ります>
すべてのガバナンス提案は、投票に進む前にメインネットのフォークに対してシミュレーションされるべきです。
4. 緊急オラクルのオーバーライド(ガードレール付き)
// Guardianはガバナンス投票なしでオラクルの消費を一時停止できる
// しかし、任意の価格を設定することはできない — 停止のみ
function pauseOracle(address asset) external onlyGuardian {
oraclePaused[asset] = true;
emit OraclePaused(asset, msg.sender);
}
// 停止中のオラクルは最後に知られていた正しい価格を使用する
function getPrice(address asset) external view returns (uint256) {
if (oraclePaused[asset]) {
return lastValidPrice[asset]; // 古い値だが、致命的に間違っているわけではない
}
return _fetchAndValidatePrice(asset);
}
5. Solana相当:Pyth価格の信頼区間
Pythオラクルを使うSolanaプログラムには、EVMプロトコルが学ぶべき内蔵の防御があります:
use pyth_sdk_solana::PriceFeed;
pub fn validate_pyth_price(price_feed: &PriceFeed) -> Result<u64> {
let price = price_feed.get_price_no_older_than(
Clock::get()?.unix_timestamp,
MAX_STALENESS_SECONDS
).ok_or(ErrorCode::StalePythPrice)?;
// Pythは信頼区間(confidence interval)を提供する — それを使う
// 信頼区間が価格の5%より広い場合、そのデータは信頼できない
let confidence_ratio = (price.conf as u128) * 10000 / (price.price.unsigned_abs() as u128);
require!(confidence_ratio < 500, ErrorCode::LowConfidencePrice);
返却形式: {"translated": "翻訳されたHTML"}// 追加: 負の価格または0の価格を拒否する
require!(price.price > 0, ErrorCode::InvalidPrice);
Ok(price.price as u64)
}
気まずい質問
プロトコルチーム向け:
- メインネットのフォークに対して、すべてのガバナンス提案をシミュレートしていますか?
- あなたのオラクル設定には、自動の妥当性(サニティ)テストがありますか?
- 重大なオラクルエラーを5分以内に修正できますか?
- できない場合、あり得ない価格変動に対して自動で停止(サーキットブレーカー)する仕組みはありますか?
AI支援開発向け:
- AIが生成したオラクル設定を、価格のバリデーションテストで検証していますか?
- レビュー過程で、AIが生成したコードをより厳密に扱い、決して甘くしない運用になっていますか?
- どの金融計算でも「人間が数学を検証する」ステップはありますか?
DeFiユーザー向け:
- 複数のオラクルインシデントが起きたことのあるプロトコルを利用していますか?
- 緊急時の修正を妨げるガバナンスのタイムロックを理解していますか?
- ポジションは、3%のオラクルエラーで清算されてしまうほどレバレッジが高くありませんか?
結論
Moonwellのエクスプロイトは、AIがコーディングに不向きだったことが原因ではありませんでした。人間でもAIでも、どの開発者であれば導入し得た「掛け算ステップの欠落」があり、どちらにせよそれを見つけられないテスティング基盤が組み合わさっていたことが原因です。
本当の教訓はこうです:DeFiのテスト文化は、危険にさらされている資本の規模に対してまだ不十分です。 単一の価格フィードの掛け算の欠落による178万ドルの損失。ファズィングは不要。フラッシュローンも不要。リ・エントランシーも不要。ただ、別の数と掛け算されるべきたった1つの数が掛け算されなかっただけです。
次のオラクル設定ミスは、すでにどこかにデプロイされています。問題は、それがガバナンスによる実行の前に見つけられるテストがあるかどうかです。
これは DeFi Security Research シリーズの一部です。以前: The Aave CAPO Oracle Incident、Custom Forta Detection Bots。