\#8 バニー事件:繰り返し行われた少額引き出しが、丸め誤差によって840万ドルの流出を引き起こす

\#8 バニー事件:繰り返し行われた少額引き出しが、丸め誤差によって840万ドルの流出を引き起こす

2025年9月2日、Bunni V2プロトコルは洗練されたエクスプロイトの被害に遭いました [1]。攻撃者は、その流動性会計メカニズムにおける重大な脆弱性を悪用し、イーサリアム上のUSDC/USDTプール [2] およびUnichain上のweETH/ETHプール [3] の2つの流動性プールから約840万米ドルを不正に引き出しました。

根本原因は、流動性除去時のアイドルプール残高の更新におけるプロトコルの丸め誤差でした。この誤差により、コントラクト内の総流動性が大幅に過小評価され、理論上の流動性と実際の流動性の間に悪用可能な乖離が生じました。攻撃者はその後、この乖離を利用して利益を得るために、精密なサンドイッチ攻撃を実行しました。

このインシデントはBunniプロトコルに直接的な甚大な財務的損失をもたらし、同プロトコルは2025年10月23日に破産を宣言しました [4]。

背景

Bunni V2は、Uniswap V4上に構築された自動マーケットメーカー(AMM)プロトコルです。コアロジックをフックメカニズムを通じて実装し、Uniswap V3の集中流動性アルゴリズムに革新を加え、流動性プロバイダー(LP)に対して資本効率の向上を目指しています [5]。

具体的には、このプロトコルは主にリレバレッジ(再担保)機能とリバランス(再調整)メカニズムを通じてLPの収益を向上させます。前者は流動性を外部の収益創出プロトコルに割り当て、基盤となる流動性を確保しつつ追加の外部収益を獲得します。後者は価格帯全体にわたる流動性の配分を継続的に最適化し、資本の能動的な活用を増加させて手数料収入を増大させます。これら2つのメカニズムは、集中的な流動性モデルを基盤とするプロトコルのコアイノベーションを形成しています。

リレバレッジ(Rehypothecation)

流動性プロバイダーの収益を向上させるために、Bunni V2はリレバレッジ戦略を採用しています。この戦略は、資金を異なるポジションに割り当てます。

  • rawBalance(生残高): トークンのプール準備金の一定部分は、Uniswap V4のcontract PoolManager内に直接保存されます。これは、スワップを容易にするための即時利用可能な流動性として機能します。
  • reserves(準備金): 残りの部分は、指定されたERC4626ボルトに預けられます。これにより、ユーザーはこれらの資産に対して追加の外部収益を得ることができます。

したがって、プールの総資産は以下のように定義されます:プール資産 = rawBalance + reservesの基底金額

リバランス(Rebalancing)

手数料収入を増やすために、Bunni V2はリバランスメカニズムを実装しており、これは時間加重平均価格を監視します。価格変動が閾値を超えると、流動性配分関数(LDF)に従って、流動性は異なる価格帯に再配分されます。

この再配分は、LDFによって必要とされるトークン比率を変更する可能性があり、一方のトークンに余剰が生じます。この余剰はアイドルバランスとして定義されます。

したがって、流動性は2つの部分に分割されます。

  • Active Balance(アクティブバランス): LDFによって割り当てられ、流動性計算に参加する部分。
  • Idle Balance(アイドルバランス): アクティブ流動性に使用されない余剰。

したがって、プール資産 = Active Balance + Idle Balance となります。

主要機能:流動性計算と引き出し

この攻撃は、queryLDF()およびwithdraw()という2つの重要な機能を悪用しています。queryLDF()関数はスワップのためのプールの流動性を計算し、withdraw()関数はユーザーが比例した流動性を引き出すことを可能にします。

`queryLDF()`関数

リレバレッジ戦略のため、基底資産の数量は動的であり、Bunni V2は固定の「総流動性」値を保存しません。代わりに、プロトコルはスワップが発生した際にリアルタイムの流動性を取得するためのqueryLDF()関数を提供します [6]。この関数の実行プロセスは、以下の4つのステップで構成されます。

  1. 流動性密度の照会:

    • 流動性密度関数ldf.query()を呼び出し、現在の価格ティック範囲外の流動性密度を取得します。
    • LiquidityAmounts.getAmountsForLiquidity()を呼び出して、現在のティック範囲内の密度を取得します。
    • トークン0とトークン1の総流動性密度を両方向で計算し、それぞれtotalDensity0totalDensity1とします。

    注目すべきは、LiquidityAmounts.getAmountsForLiquidity()関数が切り上げを使用し、計算されたトークン数量が理論値以下にならないように保守的にしている点です。

  2. 利用可能残高の計算: 流動性計算に使用される利用可能残高は、balance0およびbalance1で表されます。アイドルバランスは、流動性計算に参加しない資金を除いた、対応するトークンの総残高から差し引かれます。 この攻撃では、プールのアイドル資金がtoken0で構成されていたため、計算式は以下のようになります。

    • balance0=rawBalance0+reserve0idleBalancebalance0 = rawBalance0 + reserve0 - idleBalance

    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1

  3. 実効流動性の見積もり:

    • 各トークンがその実際の利用可能残高(balance0またはbalance1)と計算された総密度(totalDensity0またはtotalDensity1)に基づいてサポートできる流動性を見積もります。
    • 2つの見積もりのうち小さい方を最終的な実効総流動性として選択します。

    計算式は以下の通りです。

    L=min(balance0totalDensity0,balance1totalDensity1)L= min(\frac{balance0}{totalDensity0},\frac{balance1}{totalDensity1})

  4. アクティブ残高の計算:

    決定された総流動性に基づいて、プロトコルは取引に利用可能なトークンの実際の数量を計算します。これはアクティブバランスとして定義されます。

`withdraw()`関数

Bunni V2は、流動性引き出しのためにwithdraw()関数を提供しています。ユーザーは、プール総資金に対する自身のシェアに比例して流動性を引き出します。プロトコルは、rawBalancereservesidleBalanceを同じ比率で更新します。調整式は以下の通りです。

(rawBalance,reserves,idleBalance)=(rawBalance,reserves,idleBalance)×(1sharestotalSupply)(rawBalance, reserves, idleBalance) \\= (rawBalance, reserves, idleBalance) \times (1-\frac{shares}{totalSupply})

ここで:

  • sharesはユーザーが引き出す流動性トークンの数です。
  • totalSupplyはそのプールの流動性トークンの総供給量です。

脆弱性分析

脆弱性は、withdraw()関数におけるアイドルバランスの調整額の計算に由来しており、そこではフロア丸め(すなわち、切り捨て)が使用されています。これにより、アイドルバランスが過大評価されます。

利用可能残高の式、balance=rawBalance+reserveidlebalancebalance = rawBalance + reserve - idle balanceを思い出してください。過大評価されたアイドルバランスは、流動性計算に使用される利用可能残高(balance0)の過小評価を直接引き起こします。結果として、見積もられた実効総流動性も過小評価されます。Bunni Exploit Post Mortem [7]によると、この流動性計算における丸め方向は意図的に採用されていました。計算された流動性値が低いほど、スワップ時の価格影響が大きくなります。

この設計は、2つのトークン間の残高比率が比較的均衡しているという重要な仮定に依存しています。十分な流動性がある通常の条件下では、各トークンに対して個別に推定された総流動性値は通常近くなります。したがって、丸め誤差の影響は限定的です。しかし、アイドルバランスを負担するトークンの利用可能残高が極端に低くなった場合、欠陥が現れます。このシナリオでは、フロア丸め誤差が大幅に増幅されます。

攻撃者は、一連の少額の引き出しを実行することでこの脆弱性を悪用し、トークン0の利用可能残高を28 weiから4 weiに丸めました。この減少は、実際に燃焼された流動性シェアの割合をはるかに超えていました。一方、トークン1の利用可能残高は比較的正常なレベルを維持しました。この不均衡が、重大なアービトラージの窓口を作り出しました。次の章で詳細な数値分析を行います。

攻撃分析

イーサリアムのトランザクション [2] を例にとると、攻撃者は3段階の攻撃を実行しました。

  • 第一段階では、攻撃者は価格操作を実行し、USDCの利用可能残高(トークン0)を大幅に減少させました。これにより、後続の丸め誤差を増幅させるために必要な初期条件が整いました。
  • 第二段階では、コアとなるエクスプロイトが、一連の少額の引き出しを通じて実行され、プロトコルはプールの実際の流動性を過小評価しました。
  • 第三段階では、攻撃者は2つの方向性スワップを実行し、プロトコルの過小評価された流動性とプールの実際の流動性の間の乖離をアービトラージして、最終的に利益を上げました。

ステージ1:価格操作とターゲットトークン残高の削減

攻撃者は3回のスワップトランザクションを実行し、USDT(トークン1)に対するUSDC(トークン0)の価格を操作し、初期ティック-1からティック5000まで押し上げました。主な目的は、プールのUSDCアクティブ残高を枯渇させ、極めて低い28 weiまで減少させることでした。これにより、次のフェーズでの後続の丸め誤差を増幅させるために必要な初期条件が整いました。

ステージ2:引き出しの悪用による流動性乖離の増幅

攻撃者は withdraw() 関数を介して44回の少額引き出しを開始しました。この関数がidleBalanceを更新する際に使用するフロア丸めにより、プロトコルのアイドルバランスは過大評価されました。これにより、queryLDF()関数におけるUSDCの利用可能残高はさらに過小評価されました。これらの繰り返し操作の後、USDCの利用可能残高は28 weiから4 weiへと異常に抑制されました。これは85.7%の実際の減少であり、削除された流動性シェアに対応する理論的な割合(すなわち、8.998105442969973e-07%)をはるかに超えていました。この時点で、プールにおけるUSDCからの推定流動性は深刻に過小評価されていました。

ステージ3:アービトラージの実行と利益の実現

攻撃者はその後、サンドイッチ攻撃に似た操作である2つの方向性スワップを実行しました。

ステップ1:攻撃者は大量のUSDTを使用してUSDCとスワップしました。この時、USDC残高の過小評価に基づいて内部流動性計算は深刻に過小評価されていました。この大規模なスワップは価格を極端に押し上げ、ティックを5,000から839,189に移動させました。

ステップ2:極端な価格形成後、攻撃者は直ちに操作を逆転させ、USDCの一部をUSDTにスワップし直しました。プールの価格はすでに深刻に歪んでいたため、queryLDF()関数からのUSDC流動性密度の戻り値は1に低下しました。これにより、USDCに基づいて推定された流動性値が、USDTに基づいて推定された値よりも大きくなりました。

プロトコルの論理では小さい方の値を選択するため、総流動性はUSDT残高によって決定されるようになりました。これにより、計算された流動性は直ちに過小評価された状態から正常なレベルに回復し、急激な増加を引き起こしました。攻撃者はこの変動を利用して、最小限のUSDCを大量のUSDTと交換し、アービトラージを完了して利益を上げました。

まとめ

このインシデントは、最終的に流動性引き出し中のアイドルバランス調整における丸め誤差によって引き起こされました。このフロア関数の設計は、流動性計算におけるセキュリティ戦略として意図されていましたが、重要な境界条件を十分に考慮していませんでした。具体的には、トークン残高が深刻に不均衡な場合、丸め誤差は非線形に増幅されます。

このインシデントは、複雑なDeFiプロトコルにおける複数のモジュール間の連動リスクを明らかにしています。個々のコンポーネントの丸め規則が保守的に設計されていたとしても、システム全体にわたる一貫したセキュリティ検証の欠如は、特定の状況下で悪用されうる重大な脆弱性につながる可能性があります。

参考文献

  1. https://x.com/bunni_xyz/status/1962833866277744953

  2. https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b

  3. https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451

  4. https://x.com/bunni_xyz/status/1981160279871558114

  5. https://docs.bunni.xyz/docs/v2/overview

  6. https://github.com/Bunniapp/bunni-v2/blob/2b303b8c1b9f8afbb169d62ba52da93d6d2171fe/src/lib/QueryLDF.sol#L40

  7. https://blog.bunni.xyz/posts/exploit-post-mortem/


BlockSecについて

BlockSecは、フルスタックのブロックチェーンセキュリティおよび暗号コンプライアンスプロバイダーです。私たちは、コード監査(スマートコントラクト、ブロックチェーン、ウォレットを含む)、リアルタイムでの攻撃傍受、インシデント分析、不正資金の追跡、AML/CFT義務の履行を、プロトコルおよびプラットフォームのライフサイクル全体にわたって支援する製品とサービスを構築しています。

BlockSecは、著名なカンファレンスで複数のブロックチェーンセキュリティ論文を発表し、DeFiアプリケーションのゼロデイ攻撃を複数報告し、2,000万ドル以上の救済をもたらすハッキングを阻止し、数十億ドルの暗号資産を保護してきました。

Sign up for the latest updates