2023 年 9 月 15 日更新:Balancer 已發布官方事後檢討報告,詳細描述了本次事件的始末,包括經驗與汲取的教訓。這份報告敘事細膩且出色,非常值得一讀。
從安全角度來看,報告揭露了兩個漏洞。第一個是我們報告中探討的無條件捨去(Rounding down)誤差,第二個是攻擊步驟 3.6 和 3.7 中發生的「重置 0 供應量時的匯率」(resets rate on 0 supply)。Balancer 的報告認為第二個漏洞是關鍵問題,第一個漏洞僅屬輔助因素。然而,我們認為這兩個漏洞對於實現獲利同樣至關重要:
第一個漏洞用於推高代幣匯率,是獲利的根本原因。如果沒有它,產生利潤將不可行。
第二個漏洞通過平衡 bb-a-tokens 的債務來促成攻擊。如果沒有它,由於 bb-a-tokens 的流動性較差,攻擊將會失敗,除非攻擊者能通過其他方式獲得這些代幣。
2023 年 8 月 22 日,Balancer 公開宣佈一項影響多個 Boosted 池的關鍵漏洞,並敦促用戶立即從受影響的池中撤回 LP。儘管 Balancer 已啟動緊急緩解程式以保護大部分 TVL,但仍有部分資金面臨風險。遺憾的是,五天後的 8 月 27 日,我們觀察到多起野外攻擊。自那時起,已有超過 212 萬美元的資產被盜。
截至撰寫本報告時(距離公告已超過三週,我們認為此時揭露是安全的),Balancer 尚未發布針對此漏洞的深入分析。在本報告中,我們旨在提供全面的分析,主要基於 其中一筆攻擊交易。
關鍵結論 (TL;DR)
- 我們的調查顯示,根本原因是
linear(線性)池中無條件捨去邏輯所導致的價格操縱。這進而不適當地影響了對應boosted(增強)池所使用的快取代幣匯率。 - 此事件強調了向從易受攻擊的源代碼派生(fork)的項目發送及時通知的迫切需求,這對整個社群來說確實是一個重大挑戰。
- 層出不窮的攻擊凸顯了主動威脅防禦的必要性,這無疑有助於減輕未來的損失。
在接下來的章節中,我們首先會提供有關 Balancer 的一些必要背景資訊。隨後,我們將對該漏洞及相關攻擊進行全面分析。最後,我們將簡要總結目前觀察到的攻擊及其相應的獲利情況。
0x1 Balancer 背景介紹
Balancer V2 [1] 是一個去中心化的自動造市商(AMM)協定,代表了可程式化流動性的一種靈活建構模組。與其他將代幣會計與池邏輯結合的 AMM 不同,Balancer 將代幣會計與管理從池邏輯中分離出來,這可以透過減少大量代幣傳輸來提高交易效率。
Balancer 支援多種類型的池。每個池都關聯一個名為 BPT(即 Balancer Pool Token)的 LP 代幣。基本上,BPT 的價值是根據所有底層代幣的總價值計算的。
Balancer 支援多跳交易(Multi-hop swaps),也稱為 batch swaps,它利用所有註冊到 Vault(保險庫)池中的最佳價格。具體來說,Vault 提供了 batchSwap 函式來促進多跳交易。
Balancer 池中的 flash swap(閃電交易)省去了傳統執行交易時所需的持有輸入代幣的需求。相反,一旦發現失衡,你可以指示 Vault 執行交易並隨後收取獎勵。
0x1.1 Balancer 中的各種池
以下我們簡要介紹一些與此漏洞相關的池概念。
-
Linear Pools(線性池):
Linear池 [2] 是 Balancer 池,旨在促進資產與其具有收益能力的包裝(wrapped)對應物之間以已知匯率進行交換。顧名思義,Linear池使用線性數學。一個linear池會持有三種代幣,包括:- 兩種資產,即
main(主)代幣和wrapped(包裝)代幣,它們具有相同的底層資產價值; - 對應的
BPT(Balancer Pool Token)。請注意,BPT是 ERC-20 代幣。
- 兩種資產,即
-
Nesting Linear Pools(嵌套線性池):Linear Pool 的 BPT 可以嵌套在另一個池中。這在基礎資產與外部池中的代幣之間建立了一條簡單的
batchSwap路徑,因為交易者可以從BPT交易到 Linear Pool 的其中一種底層代幣。 -
Composable Stable Pools(可組合穩定池):Composable Stable Pools [3] 設計用於預期在接近平價或已知匯率下持續交易的資產。可組合穩定池使用穩定數學,允許在遇到重大價格衝擊前進行大額交易,從而極大地提高了同類和相關資產類別的資本效率。
當一個池允許交易往返於其自身的 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)是一個可組合穩定池,促進三種穩幣(即 USDC、USDT 和 DAI)之間的交易,同時將閒置流動性發送到 Aave。其底層的 linear 池為:
bb-a-USDC(包含 USDC 和包裝 aUSDC)bb-a-USDT(包含 USDT 和包裝 aUSDT)bb-a-DAI(包含 DAI 和包裝 aDAI)
具體而言,bb-a-USD 是一個可組合穩定池的集合,包含了三個不同 linear 池的池代幣,且每個 linear 池都有一個相關聯的穩定幣:DAI、USDC 和 USDT。下圖(引自官方文件 [5])展示了 bb-a-USD 的結構:

0x1.3 如何計算 BPT 的價格
一個自然而然的重要問題是,當用特定數量(即 amountIn)的 BPT 兌換另一種代幣的特定數量(即 amountOut)時,如何確定 BPT 的價格。
Balancer 對其不同池所採用的數學公式提供了詳細的說明 [6, 7]。為了簡單起見,我們在此抽象並總結最相關的概念。
以 linear 池為例,BPT 的價格是在 LinearPool 合約的 onSwap 函式中計算的。

該計算可以總結如下:

這裡的 tokenRate 使用以下公式計算:

是一個常數值:。
在上述公式中,分子可以簡化為 main 代幣餘額與 wrapped 代幣餘額之和,而分母則是預定義值(即 _INITIAL_BPT_SUPPLY)與 BPT 餘額之間的差值。
值得注意的是,所有相關代幣的餘額在執行計算前必須進行 名義化(nominalized),因為不同代幣可能具有不同的小數位數。具體而言,給定代幣的原始餘額將乘以對應的 縮放因子(upscale factor),該因子由 _scalingFactors 函式確定。
(1) Linear 池的縮放因子
BPT 和 main 代幣都具有一個常數的縮放因子。

(2) 像 bb-a-USD 這樣的 Boosted 池的縮放因子
boosted 池的計算稍顯複雜。具體而言,返回的縮放因子是 原始縮放因子(例如 1e18)與代幣匯率的乘積,該匯率由快取的代幣匯率(如有)取得。

快取的代幣匯率從何而來?存在一個名為 _updateTokenRateCache 的私有函式。顯然,此函式會先透過呼叫該代幣的 getRate 函式來獲取匯率,然後進行快取。

同樣以 bb-a-USDC 為例,對應 getRate 函式的核心邏輯遵循我們之前討論的公式。

請注意,有三種可能的路徑可以觸發 _updateTokenRateCache 函式:

此外,當透過 onSwap 函式執行更新時,還有一個過期檢查機制:

0x2 漏洞分析
根本原因在於 linear 池 onSwap 函式中無條件捨去邏輯所導致的價格操縱。這進而以不當方式影響了 boosted 池所使用的快取代幣匯率。
具體而言,當呼叫 _downscaleDown 函式時,amountOut 會被無條件捨去。因此,如果 amountOut 與 scalingFactors[indexOut] 之間存在巨大的幅度差異,_downscaleDown 函式的傳回值可能為零。

例如,如果我們使用 bb-a-USDC(作為 BPT)在 bb-a-USDC 池中交換 USDC(作為 main 代幣),當 amountOut 小於 1,000,000,000,000 時,傳回值將始終被無條件捨去為零。這會增加 bb-a-USDC 的餘額,因為它可以被視為單向地增加了 bb-a-USDC 的流動性。
結果,如果 BPT 是用於交易的代幣,其匯率將會上升(根據計算匯率的公式,分子保持不變而分母減少)。此漏洞可被利用來導致(巨大的)價格差異。
0x3 攻擊分析
該攻擊交易包含以下攻擊步驟:
- 從 Aave 閃電貸借入 300,000 USDC。
- 在
bb-a-USDC池中用 1.067753 USDC 兌換 0.970495 aUSDC。 - 在
bb-a-USDC和bb-a-USD池中執行batchSwap,即用 42,203USDC收穫 15,628bb-a-USDC、139,431bb-a-DAI和 248,868bb-a-USDT。詳細步驟總結在下表中(帶有小數位數):

- 將 LP 代幣兌換為相應的底層穩幣:
- 139,431
bb-a-DAI->bb-a-DAI池中的 141,127DAI - 15,628
bb-a-USDC->bb-a-USDC池中的 15,685USDC - 248,868
bb-a-USDT->bb-a-USDT池中的 253,461USDT
- 歸還閃電貸,最終獲利為:
- 114,324
DAI - 253,461
USDT - 0.970495
aUSDC
值得注意的是,攻擊者在步驟 2 中用 USDC 從 bb-a-USDC 池中抽乾了 aUSDC,這使得步驟 3 中的價格操縱變得更容易,即攻擊者只需要專注於 USDC 和 bb-a-USDC。
這裡步驟 3 起到了關鍵作用。現在讓我們深入細節,看看攻擊者為何能獲利。具體來說:
- 步驟 3.1 用於從
bb-a-USDC池中用bb-a-USDC抽乾USDC; - 步驟 3.3 和 3.4 用於將
bb-a-USDC交換為bb-a-DAI,而步驟 3.5 用於將bb-a-USDC交換為bb-a-USDT。 - 步驟 3.7 用於從
bb-a-USDC池中用USDC交換bb-a-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-USDC 和 bb-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 的公式。由於 aUSDC 的餘額在步驟 2 中已被耗盡,tokenRate 的計算可以簡化如下:


這裡 nominalMainBalance 的實際值是步驟 3.2 中發生無條件捨去而導致的。
(2) bb-a-USDC -> bb-a-USDT
步驟 3.5 使用相同的技巧來獲得更多的 bb-a-USDT,而 bb-a-USDC 和 bb-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
此外,bptBalance 在步驟 3.6 中增加,隨後 bptSupply 在步驟 3.7 中歸零。透過這種方式,可以以接近 1:1 的匯率將 USDC 兌換為 bb-a-USDC。

0x4 攻擊與獲利總結
截至本文撰寫時,我們已經在野外觀察到 數十起攻擊,導致超過 212 萬美元的損失。總括而言,這些攻擊由三個不同的帳戶執行,如下所示:

由於此漏洞,Balancer 總共損失了 約 100 萬美元。在對 Balancer 進行首次攻擊不到 12 小時後,其分叉協定 Beethoven X 也遭遇了類似攻擊,導致估計約 110 萬美元的損失。Beethoven X 受到的損失甚至比 Balancer 更大! 本次安全事件造成的累計損失總額約為 212 萬美元。
我們準備了一份完整的攻擊交易列表,請查閱此文件以獲取更多詳細資訊。
關於攻擊者的一些觀察
在分析每個網路發起的交易後,我們發現 Fantom 上的攻擊交易軌跡與 Ethereum 和 Optimism 上的交易存在顯著差異。
具體而言,除了關鍵函式中的顯著差異外,Fantom 上的攻擊者還利用了 兩個獨特的技巧來避免被 MEV 機器人搶先交易。此外,用於 Fantom 攻擊的資金 早在攻擊前 163 天就已準備就緒。
根據上述詳細觀察,我們可以推斷:
-
涉及至少兩名不同的攻擊者。
-
Fantom 上的攻擊者 是一名經驗豐富的累犯。
0x5 結論
總而言之,這是一個根源於無條件捨去邏輯的微妙漏洞。然而,利用此漏洞並非易事。具體而言,攻擊者能夠透過利用 linear 池中的無條件捨去問題來誇大快取的代幣匯率,從而操縱相應 boosted 池中的代幣價格。
此事件也強調了向那些從易受攻擊源代碼派生的項目及時通報的重要性。儘管 Balancer 發出了警告,但針對分叉協定的攻擊仍在持續,這凸顯了這些分叉項目需要隨時了解來源項目的安全更新。然而,確保這些分叉項目能獲得及時通知,對社群而言仍然是一個持續的挑戰。
此外,這一系列的連續攻擊強調了主動威脅防禦的重要性,這可以有效幫助減輕潛在的損失。
參考文獻
- [1] https://docs-v2.balancer.fi/concepts/overview/basics.html
- [2] Linear Pools:https://docs-v2.balancer.fi/concepts/pools/linear.html
- [3] Composable Stable Pools
- [4] Boosted Pools
- [5] https://docs-v2.balancer.fi/concepts/pools/boosted.html#example
- [6] https://docs-v2.balancer.fi/reference/math/linear-math.html
- [7] https://docs-v2.balancer.fi/reference/math/stable-math.html



