Back to Blog

#8 Bunni 事件:多次小額提款導致捨入誤差累計,致使 840 萬美元資金流失

Code Auditing
February 12, 2026
7 min read

2025 年 9 月 2 日,Bunni V2 協議遭受了一次精密的攻擊 [1]。攻擊者利用其流動性計算機制中的一個關鍵漏洞,從兩個流動性資金池中竊取了約 840 萬美元:以太坊上的 USDC/USDT 池 [2] 以及 Unichain 上的 weETH/ETH 池 [3]。

根本原因是協議在移除流動性過程中,更新閒置資金池餘額時出現了四捨五入錯誤(Rounding Error)。此錯誤導致合約中的總流動性被大幅低估,在理論流動性和實際流動性之間製造了可利用的差距。隨後,攻擊者執行了一次精準的夾子攻擊(Sandwich Attack),利用這一差異獲利。

此次事件直接導致 Bunni 協議遭受重大財務損失,隨後該協議於 2025 年 10 月 23 日宣佈破產 [4]。

背景

Bunni V2 是一個建立在 Uniswap V4 之上的自動做市商(AMM)協議。它通過 Hook 機制實現其核心邏輯,並在 Uniswap V3 的集中流動性演算法基礎上進行了創新,旨在為流動性提供者(LP)提供更高的資本效率 [5]。

具體而言,該協議主要通過「再抵押」(Rehypothecation)功能和「再平衡」(Rebalancing)機制來提升 LP 的回報。前者將流動性分配給外部收益生成協議,在確保基礎流動性的同時獲取額外的外部收益;後者持續優化流動性在價格區間的分佈,提高資本的有效利用率以增加手續費收入。這兩種機制構成了該協議在基礎集中流動性模型之上的核心創新。

再抵押 (Rehypothecation)

為了提高流動性提供者的回報,Bunni V2 採用了再抵押策略。該策略將資金分配到不同的頭寸中:

  • rawBalance:池中某代幣儲備的一部分直接存儲在 Uniswap V4 的 contract PoolManager 中。這作為進行交換(Swap)時可立即使用的流動性。
  • reserves:剩餘部分存入指定的 ERC4626 金庫中。這允許用戶在這些資產上賺取額外的外部收益。

因此,一個資金池的資產總額定義為:資金池資產 = rawBalance + reserves 的底層價值

再平衡 (Rebalancing)

為了增加手續費收入,Bunni V2 實施了再平衡機制,該機制會監控時間加權平均價格。當價格變化超過閾值時,流動性會根據流動性分佈函數(LDF)在不同的價格區間內進行重新分配。

這種重新分配可能會修改 LDF 所需的代幣比例,從而在某種代幣中留下盈餘。這種盈餘被定義為閒置餘額(Idle Balance)。

因此,流動性分為兩個部分:

  • 活躍餘額(Active Balance):由 LDF 分配並參與流動性計算的部分。
  • 閒置餘額(Idle Balance):未用於活躍流動性的盈餘部分。

因此,資金池資產 = 活躍餘額 + 閒置餘額

關鍵功能:流動性計算與移除

此次攻擊利用了兩個關鍵功能:queryLDF()withdraw()queryLDF() 功能用於計算池中用於交換的流動性,而 withdraw() 功能允許用戶移除成比例的流動性。

功能 queryLDF()

由於採用了再抵押策略,底層資產的數量是動態的,且 Bunni V2 並不存儲固定的「總流動性」數值。相反,協議提供 queryLDF() 功能,在發生交換時檢索實時流動性 [6]。該功能的執行過程包含以下四個步驟:

  1. 查詢流動性密度:

    1. 調用流動性密度函數 ldf.query(),獲取當前價格刻度範圍之外的流動性密度。
    2. 調用 LiquidityAmounts.getAmountsForLiquidity() 獲取當前刻度範圍內的密度。
    3. 計算代幣 0 和代幣 1 在兩個方向上的總流動性密度,分別標記為 totalDensity0totalDensity1

    值得注意的是,LiquidityAmounts.getAmountsForLiquidity() 函數使用向上取整,以確保計算出的代幣數量保守地不低於理論值。

  2. 計算可用餘額

    用於流動性計算的可用餘額標記為 balance0balance1。閒置餘額會從對應代幣的總餘額中扣除,排除掉不參與流動性計算的資金。

    在此次攻擊中,當池中閒置資金為 token0 時,計算公式為:

    • balance0=rawBalance0+reserve0idleBalancebalance0 = rawBalance0 + reserve0 - idleBalance
    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1
  3. 估算有效流動性

    1. 根據各代幣的實際可用餘額 (balance0balance1) 和計算出的總密度 (totalDensity0totalDensity1),估算各代幣能支持的流動性。
    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() 功能在計算閒置餘額的調整量時,使用了向下取整(Floor Rounding)。這導致閒置餘額被高估。

回顧可用餘額公式,balance=rawBalance+reserveidlebalancebalance = rawBalance + reserve - idle balance。高估的閒置餘額直接導致用於流動性計算的可用餘額(balance0)被低估。隨之,估算的有效總流動性也被低估。根據 Bunni 漏洞事後報告 [7],這種流動性計算中的舍入方向是刻意採取的。較低的計算流動性值會導致交換時更高的價格影響。

這一設計依賴於一個關鍵假設:兩種代幣之間的餘額比例保持相對平衡。在擁有足夠流動性的正常情況下,兩代幣分別估算的總流動性數值通常很接近。因此,舍入誤差的影響非常有限。然而,當攜帶閒置餘額的代幣可用餘額變得極低時,缺陷就會暴露出來。在這種情況下,向下取整的誤差會被顯著放大。

攻擊者通過執行一系列小額提款,將 token0 的可用餘額從 28 wei 向下取值到 4 wei,利用了此漏洞。這一跌幅遠遠超出了實際贖回的流動性份額比例。同時,token1 的可用餘額保持在相對正常的水平。這種失衡創造了一個巨大的套利窗口。下一章將提供詳細的數值分析。

攻擊分析

以以太坊交易 [2] 為例,攻擊者執行了三個階段的攻擊:

  • 第一階段,攻擊者進行價格操縱,大幅耗盡 USDC 的可用餘額(token0)。這創造了放大後續舍入誤差所需的初始條件。
  • 第二階段,通過一系列小額提款執行核心漏洞攻擊,導致協議低估了池的實際流動性。
  • 第三階段,攻擊者執行兩次定向交換,利用協議低估的流動性與池實際流動性之間的差異進行套利,最終獲利。

第一階段:操縱價格並降低目標代幣餘額

攻擊者執行了三次交換交易,將 USDC(token0)相對於 USDT(token1)的價格從初始刻度 -1 操縱至 5000。其主要目的是耗盡池中 USDC 的活躍餘額,將其減至極低的 28 wei。這為隨後階段放大舍入誤差創造了必要條件。

第二階段:利用提款放大流動性差異

攻擊者通過 withdraw() 功能發起了 44 次小額提款。由於該功能在更新 idleBalance 時使用了向下取整,導致協議的閒置餘額被高估。這進一步在 queryLDF() 函數中低估了 USDC 的可用餘額。經過這些重複操作,USDC 的可用餘額從 28 wei 異常抑制到 4 wei。這代表實際減少了 85.7%,遠超對應已移除流動性份額的理論比例(即 0.0000008998%)。此時,池中 USDC 的估算流動性被嚴重低估。

第三階段:執行套利並實現盈利

隨後,攻擊者執行了兩次定向交換,構成了類似於夾子攻擊的操作。

第一步:攻擊者用大量的 USDT 交換 USDC。此時,內部的流動性計算根據被低估的 USDC 餘額顯得嚴重不足。這次大規模的交換將價格推向極端,將刻度從 5,000 移動到了 839,189。

第二步:在形成極端價格後,攻擊者立即進行反向操作,將部分 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 萬美元的資金,並保障了數十億加密貨幣的安全。

Best Security Auditor for Web3

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

BlockSec Audit