本文將對 Balancer 官方於 8 月披露的漏洞細節進行復盤。

作者:慢霧安全團隊,慢霧科技

背景

8 月 22 號,Balancer 官方發佈公告表示收到影響多個 V2 Boost 池的嚴重漏洞報告,只有 1.4% 的 TVL 受影響,多個池子已暫停,並通知用戶儘快提取流動性 LP。 [1] [2]

8 月 27 號,慢霧 MistEye 系統發現疑似 Balancer 漏洞被利用的攻擊交易。 [3]

由於池子無法暫停,一部分資金仍然受到攻擊的影響,Balancer 官方再次提醒使用者將受影響池子中的 LP 取回。 [4] 隨後,Balancer 官方於 Medium 發佈了 8 月披露的漏洞細節 [5],慢霧安全團隊對其進行復盤,詳情如下:

引入

Balancer 官方在其披露中簡單指出此次的問題在於,線性池的向下捨入以及可組合池的虛擬供應量導致 bptSupply 為 0。 首先讓我們來簡單瞭解一下與這次漏洞相關的 Balancer 協議中的內容。

Balancer V2 Vault

Balancer V2 [6] 協定是一種基於乙太坊的去中心化自動做市商(AMM)協定,它代表了可程式設計流動性的靈活構建塊。 其核心元件是 Vault 合約,該合約維護著所有池子的記錄,並管理代幣的記帳和轉移,甚至包括原生 ETH 的包裝和解包。 也就是說,Vault 的實現是將代幣記帳和管理與池子邏輯分開。

Vault 中有四個介面,分別是 joinPool, exitPool, swap 和 batchSwap(加入、退出和交換是分開的調用,不存在單次調用時的組合)。 其中一個突出的特點是 batchSwap,它能實現多個池子之間多次原子交換,將一個池子交換的輸出與另一個池子的輸入相連(GiveIn 和 GiveOut)。 該系統還引入了閃電交換 [7],類似於一個內部的閃電貸。

Linear Pools 線性池

Balancer 為了提高 LP 的資本效率及 warp 和 unwarp 高額開銷的問題,在 V2 中推出線性池作為解決方案,由此引入了 BPT(ERC20 Balancer Pool Token)代幣。

線性池 [8] 包含 main token(底層資產),warpped token(包裝代幣)和 BPT 代幣,通過已知匯率交換資產及其包裝的、具有收益的對應物。 包裝代幣的比例越高,收益率和資金池的資本效率就越高。 在 warp 的過程中,通常都會通過縮放因數來確保不同代幣以相同的精度進行計算。


Composable Pools 可組合池

所有的 Balancer 池都是可組合池,池子包含其他代幣,池子本身也有自己的代幣。 其中 BPT 幣指的是 ERC20 平衡池代幣,是所有池的基礎。 用戶可以在其他池內使用 BPT 代幣自由組合進行兌換。 兌換總是涉及一個池和兩個代幣:GiveIn 和 GiveOut。 In 代表送入成分代幣並接收 BPT,而 Out 意味著送入 BPT 並接收成分代幣。 如果 BPT 本身就是成分代幣,它就可以像其他代幣一樣進行交換。 這樣的實現構成了外部池中的基礎資產和代幣之間的一個簡單 batchSwap 路徑,使用者可以用 BPT 交換到線性池的底層資產,這也是 Balancer Boosted Pool [9] 的基礎。

通過以上的組合,Balancer 的可組合池就形成了。 一個 bb-a-USD 可組合穩定池由三個線性池組成,同時向外部協定(Aave)發送閑置流動性。 例如,bb-a-DAI 是一個包含 DAI 和 waDAI(包裝的 aDAI)的線性池。 當使用者需要進行 batchSwap 時(如要將 USDT 換成 DAI),交換路徑舉例如下:


1. 在 USDT 線性池中,將 USDT 兌換 bb-a-USDT(進入 USDT 線性池);

2. 在 bb-a-USD 中,bb-a-USDT 兌換 bb-a-DAI(線性 BPT 之間的交換);

3. 在 DAI 線性池中,bb-a-DAI 兌換為 DAI(退出 DAI 線性池)。

簡單瞭解過以上前置知識后,我們進入漏洞分析環節。

分析

在 8 月 27 號時,慢霧安全團隊收到 MistEye 系統識別,一筆疑似 Balancer 漏洞的在野利用發生。 交易 [3] 如下:

攻擊者首先從 AAVE 通過閃電貸借出 300,000 枚 USDC。 接著調用 Vault 的 batchSwap 操作,通過可組合穩定池 bb-a-USD 池進行 BPT 代幣的兌換計算,最終將 94,508 枚 USDC 兌換為 59,964 枚 bb-a-USDC,68,201 枚 bb-a-DAI 和 74,280 枚 bb-a-USDT。 最後將獲得的 BPT 代幣通過 Vault 合約的 exitPool 退出池子換取底層資產,償還閃電貸,並獲利約 108,843.7 美元離場。

由此可見,這次攻擊的關鍵在 batchSwap 里,而 batchSwap 中具體發生了什麼呢? 我們深入瞭解一下。

攻擊者在整個 batchSwap 過程中,先在 bb-a-USDC 池中兌換出 USDC ,接著進行 BPT 代幣間的兌換,將 bb-a-USDC 兌換為 bb-a-DAI,bb-a-USDT 和 USDC。 最後再將底層的 main 代幣 USDC 兌換為 bb-a-USDT。 也就是說,bb-a-USDC 作為關鍵的 BPT 代幣充當 GiveOut 和 GiveIn 的成分代幣。

攻擊者在第一步以固定的縮放因數在 bb-a-USDC 線性池中用 BPT 代幣兌換出 USDC main 代幣,其增加的數量記錄在池子中的 bptBalance 中。 但是在第二次 onSwap 的兌換后,我們發現,同樣的兌換過程,兌換出 USDC 的 amountOut 值卻是 0。 這是為什麼呢?


深入 onSwap 函數,我們發現在這個過程中會先做一次精度處理 nominal 化並計算出對應代幣的縮放因數。 而在接下來調用 _downscaleDown 函數時,amountOut 存在向下捨入的情況。 如果 amountOut 和 scalingFactors[indexOut] 之間的值相差很大,計算出的 _downscaleDown 值就為零。

也就是說當我們使用 BPT 代幣來兌換 main 代幣時,如果 amountOut 過小,返回值將向下捨入為零,且這個值就是小於由 scalingFactors 所計算來的 1e12。 但 amountIn 進來的 bb-a-USDC 數量仍然會加入到 bptBalance 虛擬數量當中,而此操作會增加 bb-a-USDC 池子中的餘額,可以將其看作為單邊添加 bb-a-USDC 流動性。

接著利用可組合穩定池的特性,通過 BPT 代幣之間的相互轉換,首先將 bb-a-USDC 兌換為其他 BPT 代幣。 跟進這個兌換過程,可組合穩定池的以下調用路徑 bb-a-DAI onSwap -> _swapGivenIn -> _onSwapGivenIn 先將 bb-a-USDC 依次換成 bb-a-DAI 和 bb-a-USDT。 與在線性池中不同的是,可組合穩定池在進行 onSwap 操作之前需要進行匯率的緩存更新。 從代碼中我們可以看到,在組合池中,onSwap 會先判斷是否需要更新緩存的 token 兌換率。

經過之前的兌換,bb-a-USDC 的數量發生了改變,並通過 _toNominal 名義化后的真實總量為 totalBalance 994,010,000,000,虛擬供應的 BPT 代幣為 20,000,000,000 。 可以計算出,更新后的匯率幾乎是之前線性池原始緩存兌換率 1,100,443,876,587,504,549 的 45 倍,即 49,700,500,000,000,000,000。

隨後,在線性池中將 bb-a-USDC 兌換為 USDC。 然而這一次的兌換同第二次的兌換一樣,再一次造成 amountOut 向下捨入為 0 的情況,兌換路徑和之前相同。

而接下來的這一次兌換則是反向將 USDC 兌換成 bb-a-USDC,兌換路徑為 onSwap -> onSwapGivenIn -> _swapGivenMainIn。 在這個過程中,我們發現,在計算需要兌換的 amountOut 的時候,其中對於虛擬供應量的計算,是基於兌換后的 BPT 代幣 totalsupply 與池中剩餘量之間的差值,該差值為 0。

這是因為 bptSupply 為 0,在計算 BPT Out 時直接通過調用 _toNominal 函數,而此路徑的調用使得 USDC 兌 bb-a-USDC 的兌換比例接近 1:1。

總結

batchSwap 通過多個池子之間多次原子交換,將一個池子交換的輸出與另一個池子的輸入相連(tokenIn 和 tokenOut),將 USDC 兌換為 BPT 代幣。 在這個 batchSwap 中並不會發生實際代幣轉移,而是通過記錄轉入和轉出的數量來確認最後的兌換數量。 又因為線性池是通過底層資產代幣進行兌換的,兌換方式是通過一個虛擬供應量且是固定的演演算法計算出 Rate。 因此,batchSwap 中存在兩個安全漏洞:

一是線性池的向下捨入問題,攻擊者通過捨入為池子單邊添加 main 代幣提高緩存代幣的比率,從而操縱相應可組合池中的代幣兌換率;

二是由於可組合池的虛擬供應量特性,虛擬供應量通過 BPT 代幣減去池子中的餘額來計算,在兌換的時候如果 GiveIn 是 BPT 代幣,那麼之後的供應量就會扣掉這部分,攻擊者只需要將 BPT 作為 GiveIn 來進行兌換,並將其供應量先操縱為 0 ,之後進行反向 swap,即 BPT 再作為 GiveOut 一方,這時候由於供應量是 0,演算法會按照接近 1:1 的比例低於線性池的兌換比例來進行實際兌換,使得 GiveOut 的 BPT 代幣數量間接被操控。

我們可以發現,漏洞一為兌換增加了兌換率,而反向兌換時漏洞二再反向降低兌換率,攻擊者利用了雙重 buff 獲利離場。

參考連結:

[1]https://twitter.com/Balancer/status/1694014645378724280

[2]https://forum.balancer.fi/t/vulnerability-found-in-some-pools/5102?u=endymionjkb

[3]https://etherscan.io/tx/0x7020e0ccafff2c86db3df5a2af0cccb4e931fe948f69bf20ea517b0cc99c1f15

[4]https://twitter.com/Balancer/status/1695777503699435751 [5]https://medium.com/balancer-protocol/rate-manipulation-in-balancer-boosted-pools-technical-postmortem-53db4b642492

[6] https://docs.balancer.fi/concepts/overview/basics.html

[7]https://docs.balancer.fi/reference/swaps/flash-swaps.html#flash-swaps

[8] https://docs.balancer.fi/concepts/pools/linear.html

[9]https://docs.balancer.fi/concepts/pools/boosted.html

免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。