2023 年 11 月 23 日,我們觀察到一系列針對 KyberSwap 的攻擊。這些攻擊導致了總計超過 4,800 萬美元的損失。我們的初步分析表明,此次攻擊是由刻度(tick)操縱和流動性重複計算造成的。然而,受限於篇幅,我們無法在該篇貼文中深入探討詳情。儘管隨後有其他安全研究人員進行了深入分析,但該問題的根本原因——精度損失——仍未被完整揭露。
耐人尋味的是,幾天後情節發生了變化。2023 年 11 月 30 日,在與官方進行多輪討論後,攻擊者發送了一條訊息,在外人看來充滿了挑釁,並要求完全掌控。撇開這一點不談,攻擊者還透露了一個關鍵訊息:該問題確實與精度損失有關,如下圖所示。這一揭露強化了我們調查的證據。因此,我們的目標是在本報告中提供全面的分析。

關鍵要點 (TL;DR)
-
我們的調查顯示,根本問題源於 KyberSwap 再投資過程中的舍入方向不正確。這隨後導致了錯誤的刻度計算,並最終導致流動性重複計算。
-
此次事件突顯了 DeFi 協議中精度損失問題的複雜性和隱蔽性,這對整個社群構成了重大挑戰。
-
這些攻擊的頻率嚴正提醒我們,主動防禦威脅措施的重要性,這將有助於顯著減少未來的損失。
在接下來的章節中,我們將首先提供一些關於 KyberSwap 的重要背景資訊。隨後,我們將對該漏洞及相關攻擊進行深入分析。
0x1 背景
KyberSwap[1] 是一個去中心化自動造市商 (CLAMM) 平台。 為了滿足市場對集中流動性的需求,KyberSwap Elastic[3] 基於 Uniswap V3[2] 推出,並進行了多項改進,包括引入再投資曲線(reinvestment curve),以實現流動性提供收益的自動複利。
0x1.1 刻度(Tick)與平方根價格
在類似 Uniswap V3 的 CLAMM 中,Tick 用於以離散方式標記價格,以便流動性提供者(LP)可以在固定區間內提供流動性,而不是在整個區間內(因此稱為「集中」)[4]。
為了使 LP 能夠指定具有自定義價格區間的流動性頭寸,該協議需要一種追蹤跨多個價格點的總流動性的方法。Uniswap V3 通過將可能的價格空間劃分為離散的「刻度」來實現這一點,憑此 LP 可以貢獻任意兩個刻度之間的流動性。
根據 [5],流動性可以放在任意兩個刻度之間的區間內(不需要相鄰),即一對刻度索引(下刻度與上刻度)。具體來說,每個刻度的價格(在整數索引 i 處)定義如下:
實際上,使用的是 平方根價格(記作 sqrtP 或 sqrtPrice):
也可以根據當前的平方根價格計算當前刻度:
將平方根價格與流動性 L 結合使用是避免同時變化的實用方法。具體而言,在一個刻度內進行交換時價格會改變;當跨越一個刻度,或鑄造/銷毀流動性時,流動性會改變。更詳細的解釋,請參閱 Uniswap V3 白皮書[5]。
顯然,雖然給定的刻度僅計算一個平方根價格,但多個平方根價格可能指向同一個刻度。
0x1.2 再投資曲線(Reinvestment Curve)
基於 Uniswap V3 的 CLAMM 受限於 LP 手續費池利用率以及再投資所需的巨大 Gas 費。因此,KyberSwap 採用了 再投資曲線[6] 來解決該問題:
再投資曲線的設計目的就在於在集中流動性模型中原生再投資未被利用的 LP 手續費。這意味著集中流動性頭寸的 LP 手續費無需人工管理,也不消耗額外的 Gas 即可自動複利。此外,LP 仍然可以選擇在任何時間點分別領取其自動複利的手續費收入。
再投資曲線的關鍵在於,每次交換所收取的費用會累積為池中額外的流動性,即無限區間內的 再投資流動性 (reinvestment liquidity)。再投資代幣會鑄造給 LP,且累積的再投資流動性會相應分配給 LP。此外,再投資流動性也參與交換和價格計算過程。
準確來說,與其使用恆定乘積公式:
費用在每次交換中被累積到 ΔL 中:
ΔL 的計算可以簡化為 (假設價格偏差低於閾值):
然後,交換數量和最終價格可以根據修改後的恆定乘積公式得出:
上述計算的對應程式碼展示在相應 池 的 computeSwapStep 函數中。

需要注意的是,由於再投資流動性的存在,該函數中的 liquidity 是兩個組分的總和:基礎流動性 baseL 和累計再投資流動性 reinvestL。
0x1.3 KyberSwap 中的交換(Swap)
Uniswap V3 中交換的控制流可以如下描述[5]:

相應地,上述討論的 KyberSwap 池的 swap 函數實現可以抽象為下圖:

與刻度計算相關的關鍵邏輯存在於交換的 while 迴圈中,如藍色矩形所示。具體來說,主要邏輯涉及 computeSwapStep 函數和 _updateLiquidityAndCrossTick 函數。前者計算關鍵狀態,例如給定交換的輸入和輸出數量以及 nextSqrtP,而後者處理發生 跨刻度(cross-tick) 時的情況。
傳統上,當價格上漲時,我們稱之為將刻度向右/向上移動;否則,我們稱之為刻度向左/向下移動。
為了更好地理解稍後將討論的漏洞,探索 computeSwapStep 函數的相關程式碼邏輯至關重要,如下圖所示:

首先,從第 50 行到第 57 行,調用 calcReachAmount 函數來計算達到 targetSqrtP(下一個刻度或用戶指定的目標價格)所需的輸入代幣數量。
接下來,在第 59 行到第 62 行之間,進行測試以確定是否應該跨越刻度。
具體而言,如果使用的數量 (usedAmount) 大於用戶在精確輸入交換中指定的數量 (specifiedAmount)(攻擊所使用的情況),這意味著不應該跨越刻度,且 nextSqrtP 需要從增量流動性 (deltaL) 中得出。
- 隨後,在第 70 行到第 79 行之間,ΔL (
deltaL) 是根據輸入數量、當前流動性和價格,使用estimateIncrementalLiquidity函數得出。最後,交換後的最終價格nextSqrtP是根據deltaL、輸入數量、當前價格和流動性,使用calcFinalPrice函數計算得出。
相反,如果所需數量小於用戶指定的數量(這意味著 nextSqrtP > 0),則使用當前和目標 sqrtP 計算 deltaL,且 nextSqrtP 為下一個刻度上的 sqrtP。由於攻擊中沒有使用此分支,細節從略。
上述步驟清楚表明,如果未跨越刻度,computeSwapStep 返回的 nextSqrtP 不應大於下一個刻度的 sqrtP。然而,由於價格對流動性(基礎流動性和增量流動性)的依賴以及精度損失,攻擊者能夠在未跨越刻度的情況下,將 nextSqrtP 操縱得更大。
0x2 漏洞分析
根本原因在於 SwapMath 合約(由 computeSwapStep 函數調用)中的增量流動性計算(即 estimateIncrementalLiquidity 函數)內的舍入方向不正確,導致了有缺陷的刻度計算。這反過來又對稍後的刻度計算產生了不當影響。

有趣的是,在檢查第 188 行的註釋(藍色矩形標示)時,我們發現 deltaL 的本意是向上取整,以便對 nextSqrtP 向下取整。然而,由於第 189 行使用了 mulDivFloor 函數,deltaL 被錯誤地向下取整了。結果,nextSqrtP 被錯誤地向上取整了。
0x3 攻擊分析
攻擊者發起了多次攻擊交易,每筆交易都耗盡了多個資金池。為了簡化問題,以下討論基於攻擊交易中的首次攻擊。
核心攻擊邏輯由以下六個步驟組成:
-
通過 AAVE 閃電貸借入 2,000 WETH。
-
在受害者池 0xfd7b 中將 6.850 WETH 交換為 6.371 frxETH。此步驟用於將當前刻度和
currentSqrtP推送到目前沒有流動性的位置。
currentSqrtP似乎由攻擊者隨機選擇,且交換精確地在此價格處停止。- 此步驟後基礎流動性 (
baseL) 為零,但再投資流動性 (reinvestL) 非零。
- 向池中添加流動性,然後移除部分流動性。此步驟用於將區間和總流動性控制在所需金額。
- 刻度區間是基於
currentSqrtP選擇的。 - 攻擊所需的流動性可以從刻度區間推導出來,儘管相應的計算邏輯需要進一步探索。
- 在池中將 387.170 WETH 交換為 0.06 frxETH。此步驟用於操縱當前刻度,使得
nextTick==currentTick。
- 輸入數量是根據流動性和
currentSqrtP選擇的。
-
在池中將 0.06 frxETH 交換為 396.244 WETH。請注意,交換方向與上一步相比是相反的。在此步驟中,流動性被重複計算,從而使交換獲利並隨後耗盡資金池。
-
償還閃電貸,並收穫 6.364 WETH 和 1.117 frxETH。
顯然,最後兩次交換(步驟 4 和步驟 5)是操縱刻度計算並使交換獲利以耗盡資金池的核心攻擊步驟。我們將在以下小節中詳細說明。
需要注意的是,步驟 3 對於操縱流動性至關重要。由於需要通過舍入運算進行精確的刻度操縱,通過直接添加流動性來實現該目標是不可行的。移除流動性是為了精確控制攻擊者所需區間內的流動性。
0x3.1 步驟 4:操縱當前刻度和 currentSqrtP
在執行步驟 3 後,攻擊者已為操縱準備好了刻度區間和流動性。具體為:
currentSqrtP處於所需位置- 當前刻度 = 110,909,下一個刻度 = 111,310,環繞著
currentSqrtP
此步驟進行 WETH 到 frxETH 的交換。在 computeSwapStep 函數中,我們有以下執行軌跡:

如上圖所示,達到目標(即下一個刻度)所需的數量將通過調用 calcReachAmount 函數計算:
usedAmount=calcReachAmount(liquidity,currentSqrtP,targetSqrtP)
注意,此計算可以在交換前得出。通過小心選擇 specifiedAmount (usedAmount = specifiedAmount + 1),攻擊者控制了交換,使得未達到目標(即下一個刻度 111,310),導致 nextSqrtP = 0。
在這種情況下,因為沒有跨越刻度,nextSqrtP(即最終價格)需要從增量流動性(作為交換手續費累積)中得出。
先計算費用帶來的增量流動性 deltaL:
deltaL=estimateIncrementalLiquidity(absDelta,currentSqrtP)
然後是最終價格 nextSqrtP:
nextSqrtP=calcFinalPrice(absDelta,liquidity,deltaL,currentSqrtP)
回顧上一節討論的舍入方向錯誤,此處 deltaL 被錯誤地向下取整,導致 nextSqrtP 被向上取整。具體來說,在這種情況下,基於相同的 absDelta (387,170,294,533,119,999,999),計算結果因舍入方向不同而產生差異:

因此,在步驟 4 的刻度操縱後,當前狀態總結如下:
currentSqrtP為 20,693,058,119,558,072,255,665,971,001,964,略大於刻度 111,310 的sqrtP(刻度 111,310 的 sqrtP = 20,693,058,119,558,072,255,662,180,724,088)。- 當前刻度 = 111,310,下一個刻度 = 111,310

如上圖所示,步驟 4 中的交換狡猾地誤導了資金池,使其認為未跨越刻度 111,310。然而,實際上,currentSqrtP 的確大於刻度 111,310 的 sqrtP。
0x3.2 步驟 5:流動性重複計算
基於步驟 4 的操縱,步驟 5 中的攻擊邏輯相對簡單直接。在此階段,攻擊者策劃了一次從 frxETH 到 WETH 的反向交換,這會將刻度和 currentSqrtP 向左移動。具體而言,在迴圈中兩次調用 computeSwapStep 函數,最終以未預料到的方式觸發了流動性重複計算[7],從而產生了額外利潤。

如上追蹤圖所示:
-
在第一次調用
computeSwapStep函數時,currentSqrtP被移至刻度 111,310 的sqrtP。這是一次極小的交換,僅使用了 3 wei 的 frxETH 便實際達到了刻度 111,310。隨後,在_updateLiquidityAndCrossTick函數內,當前刻度理應跨越刻度 111,310(向左/向下移動),儘管在步驟 4 中它並沒有真正向右/向上跨越刻度 111,310。這導致刻度 111,310 處的流動性被 重複計算。 -
在第二次調用
computeSwapStep函數時,之前的流動性重複計算可能會導致額外利潤的潛力。具體來說,通過利用這次流動性重複計算,最終步驟中的交換價格發生了偏差,導致輸出更大數量的 WETH,從而產生利潤。
0x4 攻擊與利潤總結
截至本文撰寫時,我們已在野外觀察到針對不同鏈(包括 Ethereum、Optimism、Polygon、Arbitrum、Avalanche 和 Base)的多次攻擊,造成的損失超過了 4,800 萬美元。這些攻擊由不同的攻擊者發起,細節如下:
這些攻擊交易的完整清單已收集在我們準備的文件中。請參閱該文件以獲取更詳細的資訊。
0x5 結論
總之,這是一個源於不當舍入邏輯的隱晦漏洞。攻擊手段極為複雜。事實上,今年我們已經觀察到一系列與精度損失問題相關的安全事件,這對整個社群構成了重大挑戰。
再次說明,這些持續的攻擊展示了主動防禦威脅的重要性,這一策略可以有效幫助減輕潛在損失。
參考資料
[1] https://docs.kyberswap.com/
[2] https://blog.uniswap.org/uniswap-v3
[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic
[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism
[5] https://uniswap.org/whitepaper-v3.pdf
[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve



