徹底分析:バランサーV2のエクスプロイト

2025年11月3日、バランサーV2といくつかのフォークされたプロジェクトが悪用され、1億2500万ドル以上の被害が出た。このブログでは、この事件の詳細な技術的分析を行っています。

徹底分析:バランサーV2のエクスプロイト

2025年11月6日更新バランサーは正式な速報を発表した[6]。我々の分析で特定された根本原因を確認した。

2025年11月3日、Balancer V2のComposable Stable Poolsは、複数のチェーンにまたがる複数のフォークされたプロジェクトとともに、協調的なエクスプロイトを受け、総額1億2500万ドル以上の損失を被りました。BlockSecはいち早く警告を発し[1]、その後初期分析を発表しました[2]。

これは非常に巧妙な攻撃でした。私たちの調査により、根本的な原因は不変量計算の精度低下に起因する価格操作であり、その結果BPT(バランサープールトークン)の価格計算が歪められたことが明らかになりました。この不変量操作により、攻撃者は単一のバッチスワップを通じて特定の安定したプールから利益を得ることができました。一部の研究者は洞察に満ちた分析を提供していますが、特定の解釈は誤解を招きやすく、根本的な原因と攻撃プロセスはまだ完全には解明されていません。このブログでは、このインシデントに関する包括的かつ正確な技術的分析を紹介することを目的とする。

主要な要点 (TL;DR)

根本原因:丸め誤差と精度損失

  • アップスケーリング処理では一方向の丸め(下への丸め)が使用され、ダウンスケーリング処理では双方向の丸め(上への丸めと下への丸め)が使用されます。
  • この不一致は、慎重に作られたスワップパスを通して悪用された場合、丸めが常にプロトコルに有利であるべきという標準原則に違反する精度の損失を生み出します。

**悪用の実行

  • 攻撃者は、反復回数や入力値を含むパラメータを意図的に細工し、精度損失の影響を最大化した。
  • 攻撃者は検出を回避するために2段階のアプローチを使用した。まず、1つのトランザクション内でコアとなるエクスプロイトを実行し、すぐに利益を得ることなく、次に別のトランザクションで資産を引き出すことで利益を実現した。

運用上の影響と増幅について

  • このプロトコルは、ある制約のために一時停止することができませんでした[3]]。オペレーションを停止できないことがエクスプロイトのインパクトを悪化させ、多数の後続攻撃や模倣攻撃を可能にした。

以下のセクションでは、まずバランサーV2に関する重要な背景情報を提供し、次に特定された問題と関連する攻撃を詳細に分析します。

0x1の背景

バランサーV2のコンポーザブル安定プール

この攻撃で影響を受けたコンポーネントは、バランサーV2プロトコルのコンポーザブル・ステーブル・プール[4]である。これらのプールは、ほぼ1:1のパリティを維持する(または既知の為替レートで取引される)と予想される資産のために設計されており、価格への影響を最小限に抑えて大規模なスワップを可能にすることで、同種または相関性のある資産間の資本効率を大幅に改善します。各プールには独自のバランサープールトークン(BPT)があり、これは対応する原資産とともに、プールにおける流動性プロバイダーの取り分を表します。

  • このプールは(CurveのStableSwapモデルに基づく)Stable Mathを採用し、不変量Dはプールの仮想合計値を表します。
  • BPT価格は次のように近似できる:

上記の計算式から、Dを書類上小さくすることができれば(実際の資金損失がなくても)、BPT価格は安く見えることがわかる。

batchSwap()とonSwap()

バランサーV2はbatchSwap()関数を提供し、Vault[5]内でのマルチホップスワップを可能にします。この関数に渡されるパラメータで決まるスワップタイプは2つあります:

  • GIVEN_IN ("Given In"):呼び出し元が入力トークンの正確な量を指定し、プールが対応する出力量を計算する。
  • GIVEN_OUT ("Given Out"): 呼び出し元が希望する出力量を指定し、プールが必要な入力量を計算する。

通常、batchSwap() は、onSwap() 関数を介して実行される複数のトークン間スワップから構成されます。以下は、SwapRequest に GIVEN_OUT スワップ・タイプが割り当てられた場合の実行パスの概要です(ComposableStablePool は BaseGeneralPool を継承していることに注意してください):

以下は、GIVEN_OUTスワップタイプのamount_inの計算を示しており、これは不変量Dを含んでいる。

スケーリングと丸め

異なるトークンバランス間の計算を正規化するために、バランサーは以下の2つの処理を行います:

  • アップスケーリング:アップスケーリング: 計算を行う前に、残高と金額を統一された内部精度までスケールアップします。
  • ダウンスケーリング:結果を本来の精度に戻し、方向性のある四捨五入を適用する(たとえば、プールが充電不足にならないように、入力量は通常切り上げられ、出力量は切り捨てられることが多い)。
この矛盾の理由は不明である。upscale()関数のコメントによると、開発者は単一方向の丸めの影響は最小限であると考えている。

例えばスワップでは、トークンinの残高は切り上げられる // べきである。 // トークンinの残高は切り上げられ、トークンoutの残高は切り下げられる。これは、すべての金額について // 同じ方向に丸める唯一の場所である。 // この丸めによる影響は最小限であると予想されるためです。 // scalingFactor()`がオーバーライドされない限り、丸めエラーは発生しません)。

0x2 脆弱性の分析

根本的な問題は、BaseGeneralPool._swapGivenOut()関数でアップスケーリング中に実行される切り捨て処理に起因する。特に、_swapGivenOut()は、_upscale()関数を通してswapRequest.amountを不正に切り捨てます。その結果丸められた値は、_onSwapGivenOut()によってamountInを計算するときにamountOutとして使用されます。この動作は、丸めはプロトコルに利益をもたらす方法で適用されるべきであるという標準的な慣行と矛盾しています。

したがって、あるプール (wstETH/rETH/cbETH) に対して、計算された amountIn は、実際に必要なインプットを過小評価する。これにより、ユーザーはより少量の原資 産(例えば wstETH)を別の原資産(例えば cbETH)と交換することができ、その結果、 有効流動性が低下して不変量 D が減少する。その結果、対応する BPT の価格 (wstETH/rETH/cbETH) は、BPT の価格 = D / totalSupply となるため、デフレとなる。

0x3 攻撃の分析

攻撃者は、検知リスクを最小化するために2段階の攻撃を実行した:

  • 第一段階では、コアとなるエクスプロイトが単一のトランザクション内で実行され、直ちに利益を得ることはなかった。
  • 第2段階では、攻撃者は別個のトランザクションで資産を引き出すことで利益を得た。

第1段階はさらに、パラメータ計算とバッチスワップという2つの段階に分けることができます。以下では、Arbitrum上の攻撃トランザクション(TX)の例を用いてこれらのフェーズを説明する。

パラメータ計算フェーズ

このフェーズでは、攻撃者はオフチェーンでの計算とオンチェーンでのシミュレーションを組み合わせ、Composable Stable Poolの現在の状態(スケーリング係数、増幅係数、BPTレート、スワップ料、その他のパラメータを含む)に基づいて、次の(バッチスワップ)フェーズで各ホップのパラメータを正確に調整する。興味深いことに、攻撃者はこれらの計算を補助するために補助契約も導入しており、これはフロントランニングのリスクを減らすことを意図しているのかもしれない。

最初に攻撃者は、各トークンのスケーリング係数、増幅パラメータ、BPTレート、スワップ手数料のパーセンテージなど、ターゲットプールに関する基本情報を収集する。その後、攻撃者はtrickAmtと呼ばれるキー値を計算します。これは、精度の損失を誘発するために使用されるターゲットトークンの操作量です。

ターゲットトークンのスケーリングファクターをsFとすると、計算は以下のようになる:

次の(バッチスワップ)フェーズのステップ2で使用されるパラメータを決定するために、攻撃者は補助コントラクトの0x524c9e20関数に対して、以下のcalldataで後続のシミュレーションコールを行った:

uint256[] balances; // プール・トークンの残高(BPTを除く) uint256[] scalingFactors; // 各プールトークンのスケーリング係数 uint tokenIn; // このホップのシミュレーションの入力トークンのインデックス uint tokenOut; // このホップのシミュレーションの出力トークンのインデックス uint256 amountOut; // 希望する出力トークン量 uint256 amp; // プールの増幅パラメータ uint256 fee; // プールスワップ料金のパーセンテージ

そしてリターンデータは

uint256[] balances; // スワップ後のプールトークン残高(BPTを除く

具体的には、初期残高と反復ループの回数はオフチェーンで計算され、攻撃者のコントラクトのパラメータとして渡されます(それぞれ100,000,000,000と25と報告されています)。各反復は3回のスワップを行う:

  • スワップ1:スワップ方向が0→1であると仮定して、ターゲット・トークンの量をtrickAmt + 1にプッシュする。
  • スワップ2:ターゲット・トークンのtrickAmtとのスワップを継続し、_upscale()呼び出しで切り捨てをトリガする。
  • スワップ3: スワップバック操作(1 → 0)を実行する。スワップされる金額は、プール内の現在のトークン残高から、小数点以下2桁を切り捨て、つまり10^{d-2}$の倍数に切り捨てる。例えば、324,816→320,000である。
    • StableMathの計算で使用されるニュートン・ラプソン法のため、このステップが失敗することがあることに注意。これを軽減するために、攻撃者は2回の再試行を実装し、それぞれ元の値の9/10のフォールバックを使用する。 攻撃者の補助契約は、"BAL "スタイルのカスタムエラーメッセージが含まれていることからもわかるように、Balancer V2のStableMathライブラリから派生しています。

バッチ交換フェーズ

次に、batchSwap()操作は3つのステップに分けることができる:

  • ステップ1: 攻撃者はBPT (wstETH/rETH/cbETH)を原資産と交換し、1つのトークン(cbETH)の残高を四捨五入境界(amount = 9)ギリギリまで正確に調整する。これにより、次のステップで精度を落とす条件が整う。

  • ステップ2: 攻撃者は次に、別の原資産(wstETH)とcbETHを、細工した量(= 8)を使ってスワップします。トークン量をスケーリングする際に四捨五入するため、計算されたΔxはわずかに小さくなり(8.918から8)、Δyが過小評価され、その結果不変量(CurveのStableSwapモデルのD)が小さくなります。BPT価格 = D / totalSupplyなので、BPT価格は人為的にデフレになります。

  • ステップ3:攻撃者は原資産をBPTにリバーススワップし、バランスを回復させながら、デフレになったBPT価格から利益を得る。

0x4 攻撃と損失

攻撃とそれに対応する損失を以下の表にまとめました。

0x5 結論

このインシデントでは、バランサーV2プロトコルとそのフォークされたプロジェクトを標的とした一連の攻撃トランザクションが発生し、多額の金銭的損失が発生した。最初の攻撃の後、複数のチェーンで多数の後続トランザクションと模倣トランザクションが観測された。この事件は、DeFiプロトコルの設計とセキュリティにとって、いくつかの重要な教訓を浮き彫りにした:

  • 丸めの動作と精度損失:アップスケーリング動作で使用される一方向丸め(切り捨て丸め)は、ダウンスケーリング動作で使用される双方向丸め(切り上げと切り捨て丸め)とは異なる。同様の脆弱性を防ぐために、プロトコルはより高精度の演算を採用し、強固な検証チェックを実装すべきである。丸め処理は常にプロトコルに有利に働くべきであるという標準的な原則を守ることが不可欠である。

  • エクスプロイトの進化攻撃者は、検出を回避するために設計された、洗練された2段階のエクスプロイトを実行した。第 1 段階では、攻撃者は 1 回のトランザクション内でコアとなるエクスプロイトを実行し、すぐに利益を得ることはなかった。第二段階では、攻撃者は別個のトランザクションで資産を引き出すことで利益を得ました。この事件は、セキュリティ研究者と攻撃者の間で進行中の軍拡競争を再び浮き彫りにした。

  • 運用意識と脅威への対応このインシデントは、継続的な攻撃や模倣的な攻撃による潜在的な損失を軽減するために、初期化および運用状況に関するタイムリーなアラート、ならびにプロアクティブな脅威検出および予防メカニズムの重要性を強調している。

運用とビジネスの継続性を維持しながら、業界参加者はBlockSec Phalconを最後の防衛ラインとして活用し、資産を保護することができます。BlockSecの専門家チームは、お客様のプロジェクトの包括的なセキュリティ評価を実施する準備が整っています。

参考

[1] https://x.com/Phalcon_xyz/status/1985262010347696312

[2] https://x.com/Phalcon_xyz/status/1985302779263643915

[3] https://x.com/Balancer/status/1985390307245244573

[4] https://docs-v2.balancer.fi/concepts/pools/composable-stable.html

[5] https://docs-v2.balancer.fi/reference/swaps/batch-swaps.html

[6] https://x.com/balancer/status/1986104426667401241

Sign up for the latest updates