Back to Blog

週刊Web3セキュリティインシデントまとめ | 2026年3月9日〜3月15日

March 18, 2026
26 min read

過去1週間(2026/03/09 - 2026/03/15)の間に、BlockSecは8件の攻撃インシデントを検知・分析しました。推定被害総額は合計で約166万ドルです。以下の表はこれらのインシデントをまとめたものであり、各事例の詳細な分析は以降の各セクションに記載されています。

日付 インシデント タイプ 推定被害額
2026/03/09 EtherFreakers インシデント 不備のあるビジネスロジック 約2.5万ドル
2026/03/10 Alkemi インシデント 不備のあるビジネスロジック 約8.9万ドル
2026/03/10 MT インシデント 不備のあるビジネスロジック 約24.2万ドル
2026/03/11 AAVE 清算インシデント 設定ミス 約101万ドル
2026/03/11 Planet Finance インシデント 不備のあるビジネスロジック 約1万ドル
2026/03/12 AM インシデント 不備のあるビジネスロジック 約13.1万ドル
2026/03/12 DBXen インシデント 不備のあるビジネスロジック 約14.9万ドル
2026/03/15 Goose Finance インシデント 不備のあるビジネスロジック 約0.8万ドル

EtherFreakers インシデント

概要

2026年3月9日、イーサリアム上のNFTゲームであるEtherFreakersが、誤った二重計上が原因で悪用され、約2.5万ドルの被害が発生しました。ゲーム内の各NFTは「エネルギー」と呼ばれる引き出し可能なETH残高を保持しています。ゲームの仕組みとして、プレイヤーはattack()を使用して、あるNFTで別のNFTを捕獲し、ターゲットのエネルギーを請求することができます。しかし、コントラクトはターゲットの残高の支払いは行うものの、会計処理を完了させる前にNFTを転送していました。転送フックが支払い前の古いデータを読み取り、その一部を全体配当プールにフィードバックしたことで、新しいETHの裏付けがないままプールが膨張しました。攻撃者はこの捕獲メカニズムをループさせることで全体インデックスを吊り上げ、一連のNFTから膨張した残高を引き出しました。

背景

EtherFreakersはオンチェーンNFTゲームであり、各NFT(「Freaker」と呼ばれます)は「エネルギー」という引き出し可能なETH残高を保持しています。システムは配当プールの仕組みで動作しており、特定のアクションが発生すると、そのETHの一部がすべてのFreakerに比例配分されます。各Freakerが請求可能なETHは、全体のアキュムレーターであるfreakerIndexと、トークンごとのシェアウェイトであるfortuneの組み合わせによって追跡されます。

具体的には、会計公式は energyOf = basic + (freakerIndex - index) * fortune となります。freakerIndex_dissipateEnergyIntoPool(amount)が実行されると増加し、そのamountの80%が全Freakerに、20%がクリエイターに分配されます。charge()を介した直接入金はbasicのみを増加させ、freakerIndexには影響しません。したがって、freakerIndexの増加は常に、システムに入る実際のイーサリアムに裏付けられている必要があります。もしfreakerIndexが対応するETHの流入なしに成長すれば、Freakerはコントラクトが実際に保持している以上のイーサを償還できるようになります。

脆弱性分析

根本的な原因は、EtherFreakコントラクト(0x3A27...c0f33)における実行順序の誤りです。捕獲が成功すると、attack()関数は以下の手順を順番に実行します。

  1. 237行目: defender(防衛側)に対し、targetCharge(ターゲットNFTの全エネルギー)を直接ETH転送として支払います。これでエネルギーは消費されました。
  2. 240行目: _transfer(defender, capturer, targetId)を呼び出し、NFTを移動させます。内部的に、_transfer()はERC-721フックである_beforeTokenTransfer()を呼び出し、energyOf(targetId)の0.1%を引数として_dissipateEnergyIntoPool()を呼び出します。これが_dissipateEnergyIntoPool()の1回目の呼び出しであり、手順5がまだ行われていないため、古い値が読み取られます。
  3. 241行目: 通常のゲームロジックの一部である_dissipateEnergyIntoPool(sourceSpent)を明示的に呼び出します。これが2回目の呼び出しです。
  4. 244-251行目: sourceIdtargetIdの両方のenergyBalancesを更新します。

バグは手順2にあります。energyBalances[targetId]がまだ更新されていないため、フックは支払い前の残高を参照し、すでに消費されたエネルギーの一部を配当プールに投入してしまいます。手順1の直接的なETH支払いと手順2のプールへの投入の両方が同じエネルギーから引き出されており、新しいETHの裏付けなしにfreakerIndexが膨張しています。

(画像略)

攻撃分析

以下の分析は、取引 0x89e24d...9abd2942 に基づいています。

  • 手順1: 1,700 WETH を借り入れ。
  • 手順2: 攻撃者が管理するアドレスの下で、新規のFreakerであるトークン590591をミント。
  • 手順3: ゲームのattack(590, 591)関数を繰り返し呼び出し、捕獲成功のブランチで実行し続ける。
  • 手順4: 成功するたびにトークン591をヘルパーに送り返し、同じペアを再利用できるようにする。
  • 手順5: ループが成功するたびに、システムが保持するイーサを超えてfreakerIndexが膨張する。
  • 手順6: インデックスが十分に高くなったら、以前から管理していた一連のFreakerを排出(discharge)させる。トークンID 496 から 520 までは、それぞれ 0.278052246002402082 イーサで排出。
  • 手順7: 排出されたイーサをWETHにラップし、1,700 WETH のフラッシュローンを返済し、利益として約 7.498 WETH を保持。

結論

根本的な原因は、attack()の捕獲成功フローにおいて、ターゲットトークンのエネルギー状態が確定する前にtargetChargeが支払われてしまう点にあります。その後、_transfer()_beforeTokenTransfer()をトリガーし、支払い前の古いenergyOf(targetId)を読み取ってプールに分配してしまいます。これにより新しいイーサの裏付けなしにfreakerIndexが増加するため、ターゲットのエネルギーが支払いとプール投入の両方に重複してカウントされます。これはリエントランシー(再入可能)バグではなく、ビジネスロジック上のインフレバグです。

将来的に同様のリスクを軽減するために:

  • トランザクションがまだ確定処理中である間は、転送フック内部でミュータブルな状態から経済的価値を再計算することを避けてください。
  • 転送フックが状態変数を読み取る場合は、実行順序が結果に影響を与えないようにしてください(例:フック実行後ではなく、実行前に状態を確定させる)。

Alkemi インシデント

概要

2026年3月10日、イーサリアム上のAlkemiプロトコルが悪用され、約8.9万ドルの被害が発生しました。根本的な原因は会計エラーと不備のあるビジネスロジックです。問題のある清算ロジックにより、誰でも同じトランザクション内で自分自身のポジションを清算し、そこから利益を得ることが可能でした。さらに、会計エラーにより、清算中の攻撃者自身の担保控除が上書きされ、攻撃者は本来負担すべきコストを負わずに清算報酬を得ることができました。

背景

Alkemiはレンディングプロトコルです。借り手のポジションが担保不足になると、誰でも liquidateBorrow() を呼び出して債務の一部を返済し、割引価格で担保を差し押さえることができます。過剰な清算を防ぐため、プロトコルはトランザクションごとの返済可能額を以下の3つの値の最小値に制限しています。

  1. 借り手の現在の借り入れ残高 (currentBorrowBalance_TargetUnderwaterAsset)
  2. 清算割引を適用した後に、借り手の担保がカバーできる最大返済額 (calculateDiscountedBorrowDenominatedCollateral())
  3. アカウントを清算境界に戻すために必要な返済額 (calculateDiscountedRepayToEvenAmount())。※市場が isSupported の場合のみチェック

脆弱性分析

根本的な原因は、Alkemiプロトコル (0x4822...a888) における不備のあるビジネスロジックと会計エラーです。currentBorrowBalance_TargetUnderwaterAsset は借り手が未払い債務を抱えている限り必然的に 0 より大きく、calculateDiscountedBorrowDenominatedCollateral() が返す値も借り手が担保を持っている限り必然的に 0 より大きいため、AlkemiEarnPublic プロトコルは事実上、特定のローンが清算可能かどうかを判断するために calculateDiscountedRepayToEvenAmount() に依存しています。この関数では、清算すべき債務額は accountShortfall_TargetUser という変数に基づいて計算されるべきです。

しかし、実際の実装では、関数は代わりに closeFactorMantissa というグローバル変数を使用して返済が許可される債務の上限を計算し、その値を返しています。その結果、攻撃者は同じトランザクション内で借り入れを行い、直ちに自身のポジションを清算することが可能になっています。

加えて、liquidateBorrow() 関数において、清算人と借り手が同一のアドレスである場合、supplyBalance_TargetCollateralAsset 変数と supplyBalance_LiquidatorCollateralAsset 変数が同じストレージスロットを指します。関数はその後、同じ初期残高に基づいて「減額された残高」と「報酬として受け取った残高」を別々に計算し、続いて順番に同じストレージスロットに書き戻します。減額された残高が先に書き込まれ、報酬の残高が後に書き込まれるため、減額の効果が上書きされて消滅し、結果として報酬のみが残ります。これにより、攻撃者は利益をさらに拡大することができました。

攻撃分析

以下の分析は、取引 0xa170...6d9d に基づいています。

  • 手順1: 攻撃者が Balancer から 51e18 WETH のフラッシュローンを取得。
  • 手順2: 51e18 WETHETH にアンラップし、Alkemiプロトコルに供給。
  • 手順3: Alkemi から 39.5e18 ETH を借入。
  • 手順4: 39.5395e18 ETH を使用して自身のポジションを清算。
  • 手順5: Alkemi から 93.5e18 ETH を引き出し。
  • 手順6: フラッシュローンを返済し、43.4e18 ETH の利益を獲得。

結論

根本的な原因は、欠陥のある清算ロジックにより攻撃者が同じトランザクション内で借り入れと自身のポジションの清算を行って利益を得ることが可能であり、さらに誤った会計ロジックが攻撃者の利得を増幅させていたことにあります。

将来的に同様のリスクを軽減するために:

  • 借り手と清算人の残高更新に際しては、計算用に一時的なメモリ変数にコピーして書き戻すのではなく、ストレージ変数に対して直接操作すべきです。

MT インシデント

概要

2026年3月10日、BNBチェーン上のデフレトークンであるMT Tokenが悪用され、約24.2万ドルの被害が発生しました。根本的な原因は、不備のある取引制限ロジックと、特殊な転送条件に対する一貫性のない処理の組み合わせです。デフレフェーズ中、コントラクトはプール内の準備金が一定の閾値を超えた場合に買い操作を制限します。しかし、コントラクトは正確な額(例:2e17 MT)の転送をリファラル(紹介)紐付けアクションとして扱うため、攻撃者は買い制限を回避して初期トークンを取得できました。さらに、制限ロジックは不完全なパス検出(isBuy)に依存しており、PairからRouterへの間接スワップルートなどをカバーできておらず、ホワイトリストチェックが重要な検証を短絡させています。攻撃者は制限や手数料をトリガーすることなく MT トークンを蓄積し、管理下の流動性操作やトレードを通じて pendingBurnAmount を操作して、トークン価格が人為的に吊り上げられた異常な状態にプールを強制しました。

背景

MT TokenはBNBチェーン上のデフレトークンで、取引制限機能が組み込まれています。デフレフェーズ中、コントラクトはプール内の MT 準備金が 21,000e18 を超えると買い操作をブロックします。準備金がこの閾値を下回るとデフレフェーズは終了し、買い戻しが有効になります。MT Tokenにはリファラルメカニズムも含まれており、正確に 2e17 MT または 1e17 MT の転送は通常のトレードではなく、リファラル紐付けアクションとして扱われます。

脆弱性分析

根本的な原因は、MTコントラクト (0x037E...b449) における買い制限設計の欠陥です。通常条件では、攻撃者は制限フェーズ中にシードキャピタルとして MT を取得できないはずです。しかし、コントラクトは正確に 2e17 MT の転送を買いではなくリファラル紐付けアクションとして扱うため、攻撃者は買い制限を回避しつつ 2e17 MT を購入できます。

さらに、取引制限は isBuy ブランチに依存して購入をブロックしますが、「PairからRouter」へのパスをカバーしていません。RouterとPairの両方がホワイトリストアドレスであるため、そのような転送はホワイトリストチェックで短絡され、買い制限ロジックに到達しません。これにより、攻撃者はRouterに対して買いをルーティングし、その後流動性を取り除くことでトークンを自分自身に抽出する形で MT を取得できていました。

攻撃分析

以下の分析は、取引 0xfb57...fca6 に基づいています。

  • 手順1: 攻撃者が ~358,681e18 WBNB のフラッシュローンを取得。
  • 手順2: 2e17 MT を購入し、買い制限を回避。
  • 手順3: 4e12 WBNB2e17 MT をPairに供給して流動性追加。この転送も上記の通り手数料徴収ロジックを回避。
  • 手順4: Routerを介してPairから ~10,000,000e18 MT トークンを購入し、買い制限と手数料徴収の両方を回避。
  • 手順5: 流動性ポジションの半分を取り除き、その過程でRouterが保持するすべての MT トークンを抽出して回収した MTWBNB で売却。このステップで pendingBurnAmount が約 9,000,000e18 に操作された。
  • 手順6: 再び ~10,000,000e18 MT トークンを購入し、プールの MT 準備金を ~6,756,516e18 まで引き下げた(これは pendingBurnAmount よりも低い値)。
  • 手順7: 残りの流動性ポジションをすべて取り除き、購入した MT トークンを引き出し、distributeDailyRewards() を呼び出してプールから MT をバーン。結果として MT 準備金は 21,000e18 まで減少した。
  • 手順8: すべての MT~1,198e18 WBNB に戻し、フラッシュローンを返済して利益を確定。

結論

本攻撃は取引制限の不備により、攻撃者が正確に BINDING_AMOUNTMT トークンを購入することで買い制限を回避できたことが原因です。MT トークン取得後、攻撃者は流動性を追加し、Router経由で MT を購入し、最後に流動性を取り外してトークンを回収することで、手数料徴収ロジックと買い制限の両方を回避していました。攻撃者は売り操作を通じて pendingBurnAmount を蓄積し、バーンを実行してプール準備金を異常な状態に追い込むことで、MT を高値で売却して利益を得ました。

将来的に同様のリスクを軽減するために:

  • 転送の意味論(セマンティクス)と取引ロジックの間に厳密な分離を設けてください。

AAVE 清算インシデント

概要

2026年3月11日、AAVEはイーサリアム上で2,100万ドル規模の誤った清算の影響を受け、約101万ドルの被害が発生しました。根本的な原因は wstETH のオラクル価格が誤っていたことであり、本来健全なはずのポジションが担保不足とみなされました。その結果、ユーザーのポジションが清算され、経済的損失につながりました。

背景

AAVEは、ラップされた資産(wstETHなど)の価格付けにオラクルアダプターを使用しています。CAPO (Capped Price Oracle) アダプターは、基本となる ETH/USD 価格に変換比率 (getRatio()、すなわち1 wstETH が何 ETH の価値があるか) を乗じることで wstETH 価格を算出しています。比率操作を防ぐため、CAPOはスナップショットベースの成長制限(キャップ)を適用しています。

maxRatio = snapshotRatio + maxGrowthPerSecond x (currentTime - snapshotTimestamp)

そして価格付けの際、この限界値を超えないように出力(getRatio())を制限(クランプ)します。このメカニズムは、比率の上方へのドリフトと結果として生じる価格を効果的に制限します。

脆弱性分析

根本的な原因は、CAPOオラクルアンカー設定 (0xe1D9...61Ef) における時間と比率の不一致です。スナップショットタイムスタンプとスナップショット比率が設定されていましたが、スナップショット比率が実際の wstETH/ETH 比率よりも低く設定されていました。その結果、アダプターが計算する最大比率(maxRatio)が実際の比率を下回り、getRatio() を下方修正してしまい、wstETH/USD オラクル価格を体系的に過小評価してしまいました。この担保価格の低下により、wstETH を担保としていたポジションのヘルスファクターが低下し、本来健全なアカウントが誤って「不健全」と分類され、清算されてしまいました。

攻撃分析

以下の分析は、取引 0x9064...8a9c に基づいています。

  • 手順1: 清算人が ~6304e18 WETH のフラッシュローンを取得し、債務者を清算。
  • 手順2: 清算人がフラッシュローンを返済し、清算完了。

結論

今回の清算は誤ったオラクル価格設定が原因であり、本来健全であるべき借り手を誤って差し押さえ状態に追い込み、ポジションの清算をトリガーしてしまいました。

将来的に同様のリスクを軽減するために:

  • 更新のたびに、重要なパラメータが正しいかどうかを確認してください。
  • 誤ったパラメータを拒否し、誤った設定が適用されるのを防ぐためのバリデーションチェックを実装に追加してください。

Planet Finance インシデント

概要

2026年3月11日、Planet FinanceがBNBチェーンで悪用され、推定約1万ドルの被害が発生しました。根本的な原因は、プロトコルが借り手の記録上の借り入れ残高の増加を誤って発生した利息として扱った点にあります。これにより、攻撃者は繰り返し借り入れを行い、割引決済をトリガーすることで、記録上の負債を過少に見せることが可能でした。

背景

Planet Financeは、借り手が利息割引付きで返済できるレンディングプロトコルです。割引は階層化されており、ユーザーがステーキングした GAMMA と他の資産のステーキング価値との比率によって決まります。この比率が高いほど、返済割引も高くなります。割引スケジュールは3段階あり、0%(最低)から50%(最大)の間で設定されています。

脆弱性分析

根本的な原因は、changeUserBorrowDiscount() で借り手の割引を決済する際、プロトコル (0x4c9E...F467) が借り手の記録上の借り入れ残高の増加を新たに追加された利子と誤認したことにあります。その結果、利息に対してのみ適用されるべき割引が新規の借入元本にも誤って適用され、借り手の記録上の負債が不適切に削減されました。攻撃者は borrowchangeUserBorrowDiscount のループを繰り返すことで過剰な割引を蓄積し、オンチェーンに記録された負債を本来の借入額よりも一貫して低く保ち、最終的にその乖離から利益を得ることができました。

攻撃分析

以下の分析は、取引 0x5f45...5ec9 に基づいています。

  • 手順1: 攻撃者が 200,000e18 USDT のフラッシュローンを取得。
  • 手順2: 5,000e18 USDT を使って WBNB を購入し、その WBNB~8,726,524e18 GAMMA を購入。
  • 手順3: 取得したすべての GAMMA を gGAMMA 市場にステーキングし、残りの USDT を担保として供給。これにより返済割引が5%に増加し、その後の借り入れが可能に。
  • 手順4: borrow を呼び出した直後に updateUserDiscount を呼び出し、記録上の負債を継続的に削減。
  • 手順5: 最終的に負債を返済し、担保を回収して利益を確定。

結論

本件はPlanet Financeの changeUserBorrowDiscount() における欠陥のある割引決済ロジックが原因です。借り入れの増加を利息と誤認して割引を適用してしまうため、攻撃者は borrowupdateUserDiscount の操作を繰り返すことで記録上の負債を過少評価させ、本来の負債よりも少ない返済で利益を得ることができました。

将来的に同様のリスクを軽減するために:

  • レンディングプロトコルにおいて、利息と新規の借り入れを区別してください。

AM インシデント

概要

2026年3月12日、BNBチェーンのデフレトークンAM Tokenが悪用され、推定約13.1万ドルの被害が発生しました。AM Tokenは各売り時に流動性プールから追加でバーンを行うデフレメカニズムを実装しており、総供給量を減らすためにトークンを恒久的に削除します。しかし、このバーンは即時には実行されず、全売り額が toBurnAmount として記録され、実際のバーンは次の売りまで保留されます。この遅延により、記録と実行の間にタイムラグが生じ、攻撃者は AM を買い戻してプールの AM 準備金を toBurnAmount まで縮小させることが可能となります。次の売りが保留されていたバーンをトリガーすると、プール内のすべての AM 準備金が消滅し、価格が極端な水準に押し上げられ、攻撃者は利益のために AM を売却できました。

背景

AM TokenはBNBチェーン上のデフレトークンです。売りが発生するたびに、コントラクトはスワップに関与した AM 量を toBurnAmount として記録し、次回の売り時にliquidityプールからその記録分をバーンします。実質的に、売りは保留されたバーンをトリガーし、プールの AM 準備金を縮小させます。また、バーンを実行する前に、プロトコルは蓄積された totalTokenFeeUSDT にスワップし、手数料割り当てロジックに従って分配します。

脆弱性分析

根本的な原因は、トークンの (0x27f9...213f) 売りロジックがスワップされた全 AM 量を toBurnAmount として蓄積し、次回の売り時にのみ AM/USDT ペアからトークンを削除して pair.sync() を呼び出してバーンを実行する点にあります。この設計により、攻撃者はプールの AM 準備金を操作し、オンチェーン価格を歪めて裁定取引で利益を得ることが可能になっています。

攻撃分析

以下の分析は、取引 0xd0d1...f859 に基づいています。

  • 手順1: 攻撃者が ~27,265,119e18 USDC~361,710e18 WBNB のフラッシュローンを取得し、~100,423,811e18 USDT に交換。
  • 手順2: ~5,062e18 AM トークンを USDT に交換してコントラクトの toBurnAmount~4,303e18 に操作。
  • 手順3: USDT で AM Token を購入し、プールの AM 準備金を ~4,303e18 まで低下させる。
  • 手順4: 6 weiAM をプールに転送し、売りパスのバーンロジックをトリガー。結果、プールからすべての AM 残高がバーンされ、準備金は0になった。注:プロトコルはバーンの前に蓄積された手数料を USDT にスワップしようとする。この手数料変換パスもバーンロジックをトリガーする。バーン後に AM 準備金が0に達するため、手数料スワップは失敗するが、try/catchで囲まれているためトランザクションはリバートせず、手数料アキュムレーターのみが0にリセットされる。
  • 手順5: pool.sync() を呼び出し、残りの USDT1 wei AM をプールに転送。両トークンが同時に転送されたため、コントラクトはこれを流動性追加(addLiquidity)として扱い、toBurnAmount は蓄積されず、AM 準備金は7に更新された。
  • 手順6: 残りの AM トークンを USDT に交換。このスワップ中、Pair への AM 転送が売りパスのバーンロジックをトリガーし、AM 準備金を1まで減少させた。さらに手順4で totalFeeAmount が0にリセットされていたため、手数料の USDT 変換が行われず、人為的に吊り上げられた価格で AM を売却できた。
  • 手順7: フラッシュローンを返済して残りの利益を確定。

結論

本件はAM Tokenの不備あるバーンメカニズムが原因です。売買のたびにスワップした AMtoBurnAmount として貯め込み、次の売りで一気にバーンして pair.sync() を呼ぶ設計が、プールの AM 準備金を極限まで操作し、人為的価格で USDT を引き出す余地を攻撃者に与えました。

将来的に同様のリスクを軽減するために:

  • トランザクションごとの最大バーン量を制限し、バーンがトリガーされる頻度をレートリミットすることで、攻撃者が短期間でプールのトークン準備金を大量に消費するのを防いでください。

DBXen インシデント

概要

2026年3月12日、イーサリアムおよびBNBチェーン上のBurn-to-EarnプロトコルであるDBXenが悪用され、合計約14.9万ドルの被害が発生しました。根本的な原因は _msgSender()msg.sender の不整合にあります。burnBatch()forwarder を介して呼び出されると、バーンされた XEN 量は _msgSender() (攻撃者管理下)に記録されますが、サイクル記録は msg.senderforwarder)で更新されます。この分割により、攻撃者は古いサイクル記録に対して報酬や手数料を請求できるようになり、異常に大きな支払いが生じました。

背景

DBXenはBurn-to-Earnプロトコルであり、ユーザーは XEN トークンをバーンする代わりに DXN 報酬と累積されたプロトコル手数料のシェアを受け取ります。主要なメカニズムはサイクルごとに動作します。ユーザーが burnBatch() を呼び出すと、(1)バーンされた XEN 量が呼び出し元のアドレス(_msgSender() で識別)に記録され、(2)XEN コントラクトが onTokenBurned() 経由で DBXen をコールバックし、呼び出し元のサイクル記録(バーンサイクルおよび lastFeeUpdateCycle)を現在のサイクルに更新します。

報酬と手数料は updateStats() を通じて決算されます。報酬は、ユーザーのバーンサイクルにおける全 XEN バーン量に対するユーザーのシェアに比例します。手数料は、ユーザーの最後に記録されたサイクル以降に蓄積された累積プロトコル手数料に基づきます。どちらの計算も、ユーザーのサイクル記録が最新であることを前提としています。

脆弱性分析

根本的な原因はDBXenプロトコル (0xf5c8...2abd) における不備のあるビジネスロジックです。_msgSender() 関数は msg.senderforwarder であるかどうかをチェックします。もしそうであれば calldata の末尾20バイトを返し、この戻り値は forwarder のコンテキスト内で任意に操作可能です。しかし burnBatch()msg.sender が保持する XEN を直接バーンします。その結果、攻撃者は forwarder を介して burnBatch() を起動し、プロトコルに forwarder が保持する XEN をバーンさせ、forwarder のバーンサイクル記録と手数料更新サイクル記録の両方を現在のサイクルに更新させることができます。同時に、プロトコルはバーンされた XEN 量を _msgSender() に対応するアドレスに記録します。

その後、攻撃者が claimFees() を呼び出すと updateStats() が起動します。_msgSender() アドレスのサイクル記録は更新されていない(バーンサイクル、lastFeeUpdateCycle ともに 0 のまま)ため、updateStats() は現在のサイクルで報酬を計算し、サイクル 0 以降に蓄積された全手数料を算出することになります。これにより、攻撃者はプロトコルの全手数料履歴を巻き込んで、claimFees()claimRewards() を呼び出し利益を得ます。

攻撃分析

以下の分析は、取引 0x914a5a...b808bc37 に基づいています。

  • 手順1: 攻撃者が Forwarder コントラクトの registerDomainSeparator() を呼び出し、Forwarder.execute() の呼び出しを可能にする。
  • 手順2: Uniswap V2プールで 0.14e18 ETH13,900,000,000e18 XEN に交換。
  • 手順3: 13,900,000,000e18 XENForwarder コントラクトに転送。
  • 手順4: Forwarder.execute() を使用して、Forwarder が保持する 13,900,000,000e18 XEN を DBXen が使用することを承認。
  • 手順5: Forwarder.execute() を使用して DBXen.burnBatch() を呼び出し、13,900,000,000e18 XEN をバーン。バーン額は 0x425D3eC2DCeBE2c04bA1687504D43AFC6be7328d に記録され、onTokenBurned() 経由で Forwarder 側のサイクル記録が更新される。
  • 手順6: Forwarder.execute() を使用して DBXen.claimFees() を呼び出し、65.36e18 ETH を取得。
  • 手順7: Forwarder.execute() を使用して DBXen.claimRewards() を呼び出し、2,305.4e18 DXN をミント。

結論

本事件の根本原因は、DBXenプロトコルが _msgSender()msg.sender を一貫性なく使用していたことにあります。これら2つの値が異なる可能性があるため、プロトコルの内部会計に不整合が生じ、攻撃者がそれを悪用して利益を得ることができました。

将来的に同様のリスクを軽減するために:

  • すべてのロジックパスで一貫して _msgSender() を使用するか、msg.sender に依存する操作と _msgSender() に依存する会計が常に同一のアドレスを参照するようにしてください。

Goose Finance インシデント

概要

2026年3月15日、BNBチェーンのイールドファーミングプロトコルであるGoose Financeが約8,000ドル規模で悪用されました。根本的な原因は StrategyGooseEgg におけるシェア価格設定の順序に欠陥があることです:deposit() は収穫された報酬を会計に反映させる前にシェアをミントするため、シェア価格設定に使用される「総資産の分母」はこれらの報酬を含まず、真の価値よりも低くなります。つまり、預け入れユーザーは本来よりも多くのシェアを受け取ってしまいます。withdraw() が呼び出されると、報酬の収穫がトリガーされて総資産が増加し、各シェアの価値が高まります。攻撃者はトランザクション内で預け入れと引き出しを繰り返すことで、意図的に高値で算出されたシェアをミントし、修正(向上)された価値で引き出すことで、その差額を利益として抽出しました。

背景

Goose FinanceはBNBチェーン上のイールドファーミングプロトコルで、ユーザーの資金はヴォールトから戦略コントラクトへと流れ、MasterChef で資産をステーキングして EGG 報酬を獲得します。

関係するコンポーネントは:

  • VaultChef (0x3f64...): ユーザーのポジションを追跡し、資金を StrategyGooseEgg に転送する。
  • StrategyGooseEgg (0x0980...): sharesTotal および wantLockedTotal を用いて戦略レベルの会計を維持する。
  • MasterChef (0xe70e...): ステークされた資産を受け取り、EGG 報酬を支払う。
  • WrappedEgg (0xb815...): EGG を1:1で WEGG にラップしてステーキングする。

deposits(預け入れ)はヴォールトから戦略コントラクトへルーティングされ、MasterChef にステークされます。withdrawals(引き出し)はヴォールトから開始され、戦略コントラクトによって実行されます。

会計上の期待としては、シェア価格は価格算出時点の総資産(ステークされた元本+戦略が保持する未収穫報酬)を反映すべきです。しかし StrategyGooseEgg では、deposit()_farm() がアイドル資産を wantLockedTotal に確定させる前にシェアをミントします。また withdraw()MasterChef からの収穫をトリガーできます。この順序が脆弱性の根源です。

脆弱性分析

根本的な原因は、StrategyGooseEgg (0x0980...b26b) における報酬収穫とシェア価格設定との間の会計の不整合です。

StrategyGooseEgg では、シェア価格は wantLockedTotal を分母として算出します(shares = deposit * sharesTotal / wantLockedTotal)。これが公平であるためには、wantLockedTotal がコントラクトが保持する全資産を反映している必要があります。しかし、deposit()_farm() がアイドル報酬を合計額に加算する前にシェアをミントします。つまり分母が更新されていない状態であり、預け入れユーザーは本来よりも多くのシェアを獲得してしまいます。

さらに、withdraw()MasterChef.withdraw() を呼び出し、ステークされた元本と未収穫の EGG を戦略へ返し、リクエストされた _wantAmt 分を wantLockedTotal から差し引くだけです。そのため、収穫した報酬は wantLockedTotal に反映されず、残ったままになります。これにより実際の資産と記録上の wantLockedTotal との乖離が拡大し、その後の deposit() の価格設定をさらに不正確にします。

攻撃分析

以下の分析は、取引 0x86efdf...ce316223 に基づいています。

  • 手順1: 攻撃者がPancakeの2つのペアから EGG をフラッシュローン。
  • 手順2: VaultChef/StrategyGooseEgg への初回預け入れ (10,170,000e18 EGG)。
  • 手順3: 初回引き出し (12,593,884e18 EGG) が MasterChef から報酬を収穫。359,561e18 EGGStrategyGooseEgg にアイドル状態として残り、資産として計上されない(R > 0)。
  • 手順4: 2回目預け入れで引き出した資金を再利用 (12,593,884e18 EGG)。シェア価格はアイドル資産が決済される前に算出されるため、ここでシェアを過剰にミント。
  • 手順5: 2回目引き出し (12,826,027e18 EGG) により、過剰ミントされたシェアから利益(手順4の元本から 232,143 EGG のプラス)を確定。
  • 手順6: フラッシュローンを返済して利益を確保。

結論

本件は deposit()wantLockedTotal を更新する前にシェアをミントしてしまう順序の欠陥に起因します。預け入れは古い分母でシェアをミントし、引き出しは更新後の資産に対して行われるため、差額が利益となります。

将来的に同様のリスクを軽減するために:

  • シェアのミントおよびバーン計算の前に、報酬の確定と会計の更新を行ってください。
  • 計算時点での厳密な総資産 (staked + idle) を基準に計算してください。
  • アイドル報酬がある状態で shares_minted <= D * S / (A + R) となるような不変量テストを追加してください。

BlockSecについて

BlockSecは、フルスタックのブロックチェーンセキュリティおよび暗号資産コンプライアンスプロバイダーです。スマートコントラクト、ブロックチェーン、ウォレットのコード監査、リアルタイムでの攻撃阻止、インシデント分析、不正資金追跡、AML/CFT義務の履行など、プロトコルとプラットフォームの全ライフサイクルを支援します。

BlockSecは権威ある国際会議で複数のブロックチェーンセキュリティ論文を発表し、DeFiアプリケーションのゼロデイ攻撃の報告や、多くのハッキング事件の被害額2,000万ドル以上の救出実績、そして数十億ドル規模の暗号資産の保護を行ってきました。