Back to Blog

端数の切り捨てが招いた巨額損失:Balancer事件の徹底分析

Code Auditing
September 14, 2023
13 min read

2023年9月15日更新: Balancerは公式ポストモーテム(事後分析報告書)を公開しました。この報告書では、今回のインシデントの全容と、そこから得られた経験や教訓が詳細に記述されています。このポストモーテムは、その緻密で優れた記述により説得力があり、一読の価値が十分にあります。

セキュリティの観点から言えば、このポストモーテムによって2つのバグの存在が明らかになりました。1つ目は我々のレポートで議論した「切り捨て(Rounding down)エラー」であり、2つ目は我々のレポートの攻撃手順3.6および3.7で詳述した「供給ゼロ時のレートリセット(resets rate on 0 supply)」です。Balancer側の報告では、2つ目を最も重大な問題とし、1つ目をその要因の一つと見なしています。しかし、我々は利益を上げるための攻撃において、両方のバグが等しく重要であると考えています。

  1. 最初のバグはトークンレートを釣り上げるために使用され、利益の根本原因となっています。これがなければ、利益を生み出すことは不可能だったでしょう。

  2. 2つ目のバグは、bb-a-tokensの負債をバランスさせることでエクスプロイト(脆弱性攻撃)を実現しています。これがなければ、他の手段でこれらのトークンを入手しない限り、bb-a-tokensの流動性が不足しているため攻撃は失敗に終わっていたはずです。


2023年8月22日、Balancerは複数のBoostedプールに影響を与える重大な脆弱性が存在することを公表し、対象プールの流動性提供者(LP)に対し、直ちに資金を引き出すよう強く促しました。BalancerはTVL(預かり資産総額)の大部分を保護するための緊急緩和措置を開始しましたが、一部の資金は依然としてリスクにさらされていました。残念ながら、5日後の8月27日、我々は実際に進行中の攻撃を複数確認しました。それ以来、212万ドルを超える資産が盗まれています。

本レポート執筆時点(発表から3週間以上が経過し、公開しても安全であると判断した時点)において、Balancerはこの脆弱性に関する詳細な分析を公開していません。本レポートでは、攻撃トランザクションの一つを主軸として、包括的な分析を提供することを目的とします。

主要なポイント (TL;DR)

  • 我々の調査によれば、根本原因は***linearプールにおける切り捨てロジックに起因する価格操作***にあります。これが、対応するboostedプールで使用されるキャッシュされたトークンレートに不適切な影響を与えていました。
  • このインシデントは、脆弱なソースからフォーク(派生)したプロジェクトへの迅速な通知の重要性を強調しており、これはコミュニティ全体にとって確かに大きな課題です。
  • 相次ぐ攻撃は、プロアクティブな脅威防止の必要性を浮き彫りにしています。これは、将来的な損失を軽減するために不可欠なものです。

以下のセクションでは、まずBalancerに関する背景情報を概説します。続いて、この脆弱性と関連する攻撃について包括的な分析を行います。最後に、現在までに観測された攻撃と、それらによる利益の概要をまとめます。

0x1 Balancerの背景

Balancer V2 [1] は、プログラム可能な流動性を実現する柔軟な構成要素を備えた、分散型の自動マーケットメーカー(AMM)プロトコルです。トークンの会計処理とプールロジックがペアになっている他のAMMとは異なり、Balancerはトークンの会計・管理をプールロジックから分離しています。これにより、トークンの転送回数を減らし、スワップ効率を向上させています。

Balancerは様々なタイプのプールをサポートしています。各プールにはBPT(Balancer Pool Token)というLPトークンが紐付いています。基本的には、BPTの価値はすべての基盤トークンの合計価値に基づき計算されます。

Balancerは、Vaultに登録されたすべてのプールから最良の価格を活用する、batch swaps(マルチホップスワップ)もサポートしています。具体的には、VaultはbatchSwap関数を提供し、マルチホップスワップを容易にします。

Balancerプールのflash swapは、スワップを実行するために通常必要となる入力トークンをあらかじめ保有しておく必要をなくします。その代わり、不均衡を特定した際に、Vaultにスワップを実行させ、後から報酬を受け取ることができます。

0x1.1 Balancerの様々なプール

以下に、本脆弱性に関連するプールの概念を簡単に紹介します。

  • Linear Pools: Linearプール [2] は、ある資産と、その利回りを得られるラッピングされた対応資産を、既知の為替レートで交換できるようにするBalancerプールです。名前が示す通り、Linearプールは線形数学(Linear Math)を使用します。linearプールは以下の3種類のトークンを保持します。

    • 2つの資産、すなわち同等の基礎価値を持つmain(メイン)トークンとwrapped(ラッピング)トークン
    • 対応するBPT(Balancer Pool Token)。なお、BPTはERC-20トークンです。
  • Nesting Linear Pools(ネストされたリニアプール): Linear PoolのBPTは、他のプール内にネスト(入れ子)することができます。これにより、基盤となる資産と外側のプールのトークン間で単純なbatchSwapパスが作成されます。スワッパーはBPTからLinearプールの基盤トークンへと直接スワップできるためです。

  • Composable Stable Pools: Composable Stable Pools [3] は、等価、あるいは既知の為替レートで安定して交換されることが想定される資産のために設計されています。Composable Stable Poolsはステーブル数学(Stable Math)を使用しており、これによって価格への大きな影響を抑えながら大規模なスワップを行うことができ、同種または相関性の高い資産の交換において資本効率を大幅に高めます。

    プールは、そのLPトークンとの間で直接スワップができる場合に「コンポーザブル(構成可能)」と言えます。LPトークンを他のプールに入れる(ネストする)ことで、ネストされたプールのトークンから外側のプールのトークンへの容易なbatchSwapが可能になります。

  • Boosted Pools: Boostedプール [4] は、大規模プールにおいて遊休流動性の資本効率を向上させるために設計されています。boostedプールは実際には他のプールのサブクラスです。例えば、boostedプールはlinearプールの上に構築されます。

    Boosted Poolsは、ユーザーが一般的なトークンのためのスワップ流動性を提供しつつ、遊休トークンを外部プロトコルへ渡すことで、高い資本効率を実現するように設計されています。これにより、流動性提供者はスワップ手数料に加えて、Aaveのようなプロトコルからの利益を得ることができるようになります。

0x1.2 脆弱なBoostedプールの具体例:Balancer Boosted Aave USD

Balancer Boosted Aave USD (シンボル: bb-a-USD)は、3つのステーブルコイン(USDC、USDT、DAI)間でのスワップを容易にしつつ、遊休流動性をAaveに送信するComposable Stable Poolです。その基礎となるlinearプールは以下の通りです。

  • bb-a-USDC (USDCとラッピングされたaUSDCで構成)
  • bb-a-USDT (USDTとラッピングされたaUSDTで構成)
  • bb-a-DAI (DAIとラッピングされたaDAIで構成)

具体的には、bb-a-USDは3つの異なるlinearプールのプールトークンを格納する1つのComposable Stable Poolの集合であり、それぞれのlinearプールにはDAI、USDC、USDTという安定したトークンが紐付いています。公式ドキュメント [5] に記載されている以下の図は、bb-a-USDの構造を示しています。

0x1.3 BPT価格の計算方法

当然浮上する重要な問いは、特定の量(amountIn)のBPTを別のトークンの一定量(amountOut)とスワップする際に、BPTの価格をどのように決定するかという点です。

Balancerは、各プールで採用されている数学的公式について詳細な説明 [6, 7] を提供しています。分かりやすくするために、ここでは最も関連性の高い概念を抽出して要約します。

例としてlinearプールを挙げると、BPTの価格はLinearPoolコントラクトのonSwap関数内で計算されます。

計算式は以下のように要約できます。

amountOut=amountIntokenRateamountOut=amountIn*tokenRate

ここでtokenRate(トークンレート)は以下の公式で計算されます。

_INITIAL_BPT_SUPPLY\_INITIAL\_BPT\_SUPPLY は定数: 211212^{112} - 1 です。

上記の公式において、分子はmainトークンの残高とwrappedトークンの残高の合計として簡略化でき、分母は事前定義された値(_INITIAL_BPT_SUPPLY)とBPT残高の差となります。

重要な点として、関与するすべてのトークンの残高は、計算を行う前に**名目化(nominalized)される必要があります。トークンごとに小数点以下の桁数が異なる場合があるためです。具体的には、特定のトークンの生の残高に、_scalingFactors関数で決定される対応するアップスケール係数が掛けられます。

(1) Linearプールのスケーリング係数

BPTmainトークンの両方には、定数のスケーリング係数が設定されています。

(2) bb-a-USDのようなBoostedプールのスケーリング係数

boostedプールの計算はもう少し複雑です。具体的には、返されるスケーリング係数は生のスケーリング係数(例:1e18)とトークンレートの積であり、これは(もしあれば)キャッシュされたトークンレートから取得されます。

キャッシュされたトークンレートはどこから来るのでしょうか? _updateTokenRateCacheというプライベート関数が存在します。明らかに、この関数はまず特定のトークンのgetRate関数を呼び出してレートを取得し、それをキャッシュします。

ここでもbb-a-USDCを例にとると、対応するgetRate関数の核心ロジックは、前述した公式に従います。

_updateTokenRateCache関数をトリガーするパスは3つあります。

さらに、onSwap関数を経由するパスで更新を実行する際には、期限切れチェックが行われます。

0x2 脆弱性分析

根本原因は、linearプールのonSwap関数内にある切り捨て(rounding down)ロジックに起因する価格操作にあります。これが結果的に、boostedプールで使用されるキャッシュされたトークンレートに不適切な影響を与えています。

具体的には、_downscaleDown関数が呼び出されると、amountOutが切り捨てられます。そのため、amountOutscalingFactors[indexOut]の間に大きな差異がある場合、_downscaleDown関数の戻り値がゼロになる可能性があります。

例えば、bb-a-USDCプールでbb-a-USDC(BPTとして)を使用してUSDC(mainトークンとして)をスワップする際、amountOutが1,000,000,000,000未満になると、戻り値は常にゼロに切り捨てられます。これはbb-a-USDCの流動性を単方向にのみ追加すると見なせるため、bb-a-USDCの残高を増加させることになります。

その結果、もしBPTがスワップに使用されるトークンであれば、レート計算の公式に従ってそのレートは上昇します。分子は変わらずに分母が減少するためです。このバグは(莫大な)価格差を生み出すために悪用される可能性があります。

0x3 攻撃分析

攻撃トランザクションは、以下の攻撃ステップで構成されています。

  1. Aaveからフラッシュローン(フラッシュローン)で300,000 USDCを借り入れ。
  2. bb-a-USDCプールにおいて、1.067753 USDCを0.970495 aUSDCと交換。
  3. bb-a-USDCおよびbb-a-USDプールでbatchSwapを実行。すなわち、42,203 USDCで15,628 bb-a-USDC、139,431 bb-a-DAI、および248,868 bb-a-USDTを収穫。詳細なステップは以下の表の通りです(小数点以下含む)。
  1. LPトークンを対応する基盤のステーブルトークンと交換:
  • 139,431 bb-a-DAI -> bb-a-DAIプールで141,127 DAI
  • 15,628 bb-a-USDC -> bb-a-USDCプールで15,685 USDC
  • 248,868 bb-a-USDT -> bb-a-USDTプールで253,461 USDT
  1. フラッシュローンを返済。最終的な利益は以下の通りとなりました。
  • 114,324 DAI
  • 253,461 USDT
  • 0.970495 aUSDC

注目すべきは、攻撃者がステップ2でbb-a-USDCプールからUSDCを使ってaUSDCを引き抜いたことで、ステップ3での価格操作が非常に容易になった(攻撃者はUSDCbb-a-USDCのみに注力すればよくなった)点です。

ここでステップ3が重要な役割を果たします。なぜ攻撃者が利益を得られたのか、詳細を掘り下げてみましょう。具体的には、

  • ステップ3.1はbb-a-USDCプールからbb-a-USDCを使ってUSDCを枯渇させるために使用されました。
  • ステップ3.3および3.4はbb-a-USDCbb-a-DAIと交換するために使用され、ステップ3.5はbb-a-USDCbb-a-USDTと交換するために使用されました。
  • ステップ3.7はbb-a-USDCプールからbb-a-USDCと交換してUSDCを得るために使用されました。

ここで、ステップ3.2と3.6では、前述した切り捨てによって戻ってくる対象トークン(USDC)がありません。そのため、スワップ後も対象トークンの残高は変化せず、これはbb-a-USDCプールに対してbb-a-USDCの追加流動性を注入したことと見なせます。

明らかに、異常なスワップは主にステップ3.4、3.5、および3.7で発生しています。以下、各ステップの詳細を見ていきます。

(1) bb-a-USDC -> bb-a-DAI

ステップ3.3において、bb-a-USDCbb-a-DAIの為替レートはほぼ1ですが、ステップ3.4では為替レートが19になります。

  • ステップ3.3: 1,000,339,378,515,783,699 / 1,000,000,000,000,000,000 = 1.00
  • ステップ3.4: 139,430,482,942,020,211,267,110 / 7,300,000,000,000,000,000,000 = 19.10

前述のコードロジックを振り返ると、ステップ3.3では、以前にキャッシュされたトークンレートを返してスケーリング係数(1,012,181,365,780,643,700)を計算した後、新しい値を計算するためにレートを更新します(40,240,000,000,000,000,000)。この更新された値が、ステップ3.4で新しいスケーリング係数として使用されます。生のスケーリング係数は変わらないため(1e18)、新しいレートは約40倍になったことを意味します。

しかし、この大幅な上昇はどこから来ているのでしょうか? tokenRate計算の公式を再考しましょう。ステップ2でaUSDCの残高が枯渇しているため、tokenRateの計算は以下のように簡略化されます。

ここで、実際のnominalMainBalanceの値は、ステップ3.2で発生した切り捨てに起因するものです。

(2) bb-a-USDC -> bb-a-USDT

ステップ3.5でも同じトリックを使用してより多くのbb-a-USDTを獲得しており、bb-a-USDCbb-a-USDTの為替レートは12を超えています。

  • 248,868,905,733,352,246,491,156 / 20,000,000,000,000,000,000,000 = 12.44

(3) USDC -> bb-a-USDC

さらに、ステップ3.6でbptBalanceが増加し、ステップ3.7でbptSupplyがゼロになります。このようにすることで、ほぼ1:1の為替レートでUSDCbb-a-USDCと交換することが可能になります。

0x4 攻撃と利益の要約

本稿執筆時点で、我々は数十件もの同様の攻撃を観測しており、損失額は212万ドルを超えています。要約すると、これらの攻撃は以下のように3つの別々の口座によって実行されました。

Balancerはこの脆弱性により合計で約100万ドルの損失を被りました。Balancerへの最初の攻撃から12時間も経たないうちに、そのフォーク先であるBeethoven Xも同様の攻撃を受け、推定約110万ドルの損失を被りました。Beethoven XはBalancerよりも大きな損失を被ったのです! このセキュリティインシデントによる累積損失額は合計で約212万ドルに達しました。

これらの攻撃トランザクションの全リストは、我々が作成したドキュメントにまとめてあります。より詳細な情報についてはそちらを参照してください。

攻撃者に関する所見

各ネットワークで開始されたトランザクションを分析すると、Fantomにおける攻撃トランザクションの痕跡は、EthereumOptimismのものとは顕著に異なることが分かりました。

具体的には、主要な機能における著しい違いに加え、Fantomの攻撃者はMEVボットによるフロントランを回避するために2つのユニークなトリックを活用していました。さらに、Fantomにおける攻撃に使用された資金は、攻撃の163日前に準備されていました

上記、詳述した観察から、以下のように推測できます。

  • 少なくとも2人の異なる攻撃者が関与している。
  • Fantomの攻撃者は、経験豊富な常習犯である

0x5 結論

要約すると、これは切り捨てロジックに起因する、微妙な脆弱性です。しかし、この脆弱性を悪用することは容易ではありません。具体的には、攻撃者はlinearプールにおける切り捨ての問題を悪用してキャッシュされたトークンレートをインフレさせることにより、対応するboostedプールでのトークン価格を操作することに成功しました。

本インシデントは、脆弱なソースからフォークしたプロジェクトに対するタイムリーな通知の重要性も強調しています。Balancerの警告にもかかわらず、フォークされたプロトコルを狙った攻撃は続いており、これらのプロジェクトはソースプロジェクトからのセキュリティアップデート情報を入手し続ける必要があることが分かります。しかし、そのようなフォークされたプロジェクトへ迅速な通知を保証することは、コミュニティにとって現在も続く課題です。

さらに、一連の継続的な攻撃は、潜在的な損失を効果的に軽減するために役立つ、プロアクティブな脅威防止の重要性を強調しています。

参照

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit