Back to Blog

#3:KyberSwap 事件:利用細微計算誤差進行巧妙攻擊

Code Auditing
February 10, 2024
7 min read

2023 年 11 月 23 日,KyberSwap 遭遇了一系列攻擊,導致總損失超過 4800 萬美元。造成此問題的根本原因是 KyberSwap 在再投資過程中存在錯誤的捨入方向,進而導致了錯誤的價格刻度(Tick)計算,最終導致流動性被重複計算。

我們發布了一份詳細的報告:「又是精確度損失的悲劇:KyberSwap 事件深度分析」,深入探討了事件的細節。如需更深入的了解,建議您查閱完整分析。以下我們對該事件進行了簡要介紹,並將其列為 2023 年十大安全事件之一。

背景

KyberSwap 是一個去中心化的自動化做市商(CLAMM)平台。為了滿足集中流動性的市場需求,KyberSwap 基於 Uniswap V3 推出了 KyberSwap Elastic,並進行了一些改進,包括引入再投資曲線(reinvestment curve),從而實現流動性挖礦收益的自動複利。

1. 刻度(Tick)與平方根價格

在類 Uniswap V3 的 CLAMM 中,Tick 用於以離散方式標記價格,以便流動性提供者(LP)可以在固定範圍內而非整個範圍內提供流動性(因此稱為「集中」流動性)。

流動性可以存放在任意兩個刻度(不一定是相鄰的)之間的範圍內,即一對刻度索引(下界刻度和上界刻度)。具體而言,每個刻度(在整數索引 i 下)的價格定義如下:

實際上,使用的是 平方根價格(記為 sqrtPsqrtPrice):

也可以根據當前的平方根價格計算當前刻度:

顯然,雖然給定的刻度僅計算唯一的對應平方根價格,但多個平方根價格可能指向同一個刻度。有關更詳細的說明,請參考 Uniswap V3 和 KyberSwap 的文檔。

2. 再投資曲線

基於 Uniswap V3 的 CLAMM 面臨 LP 費用池利用率問題,且再投資需要高額 Gas 費用。因此,KyberSwap 採用了 再投資曲線 來解決這個問題。

再投資曲線的關鍵在於,每次交易收集的費用會作為額外的流動性累積在資金池中,並作為無限範圍內的再投資流動性(reinvestment liquidity)。再投資代幣會鑄造給 LP,累積的再投資流動性也會相應分配給 LP。此外,再投資流動性也參與交易和價格計算過程。

上述計算對應的代碼顯示在下述 資金池 代碼片段的 computeSwapStep 函數中。

需要注意的是,由於再投資流動性的存在,該函數中的 liquidity 是兩個分量的總和:即基礎流動性 baseL 和累積的再投資流動性 reinvestL

3. KyberSwap 中的交易

上述討論的 KyberSwap 資金池的 swap 函數實現可以抽象為下圖:

與刻度計算有關的關鍵邏輯存在於交易的 while 循環中,如藍色矩形所示。具體來說,主要邏輯涉及 computeSwapStep 函數和 _updateLiquidityAndCrossTick 函數。前者計算關鍵狀態,例如給定交易的輸入和輸出數量以及 nextSqrtP,而後者則處理發生 跨越刻度(cross-tick) 時的情況。

傳統上,當價格上漲時,我們稱之為將刻度向右/向上移動;否則,我們稱之為刻度向左/向下移動。

為了更好地理解稍後將討論的漏洞,我們必須研究 computeSwapStep 函數的相關代碼邏輯,如下圖所示:

具體來說,calcReachAmount 函數會計算目標價格 targetSqrtP 所需的輸入代幣(第 50-57 行)。如果 usedAmount 大於 specifiedAmount,則表示未跨越刻度,nextSqrtP 將由 deltaL(即增量流動性,第 59-62 行)計算得出。deltaL 使用 estimateIncrementalLiquidity 函數確定,最終價格 nextSqrtP 則使用 calcFinalPrice 函數計算(第 70-79 行)。如果所需的輸入較少,nextSqrtP 將被設定為下一個刻度的價格,但攻擊中未用到此情況。

上述步驟清楚表明,如果未跨越刻度,computeSwapStep 返回的 nextSqrtP 不應大於下一個刻度對應的 sqrtP。然而,由於價格對流動性(基礎流動性和增量流動性)的依賴以及精確度損失,攻擊者得以在未跨越刻度的情況下操控 nextSqrtP 使其變大。

漏洞分析

根本原因在於 SwapMath 合約(由 computeSwapStep 函數調用)中 estimateIncrementalLiquidity 函數內部的捨入方向錯誤,導致了有缺陷的刻度計算。這反過來又對隨後的刻度計算產生了不當影響。

有趣的是,檢查第 188 行的註釋(由藍色矩形標出)可以發現,原本的意圖是讓 deltaL 向上捨入,以使得 nextSqrtP 向下捨入。然而,由於第 189 行使用了 mulDivFloor 函數,deltaL 被錯誤地向下捨入。結果,nextSqrtP 被錯誤地向上捨入。

攻擊分析

攻擊者發起了多筆攻擊交易,每筆交易都耗盡了多個資金池。為了簡化起見,以下討論基於 攻擊交易 中的第一次攻擊。

核心攻擊邏輯由以下六個步驟組成:

  1. 通過 AAVE 閃電貸借入 2,000 WETH。

  2. 在受害者資金池 0xfd7b 中將 6.850 WETH 兌換為 6.371 frxETH。此步驟用於將當前刻度和 currentSqrtP 推送到目前沒有流動性的位置。

  • currentSqrtP 似乎是由攻擊者隨機選擇的,交易精確停止在該價格。
  • 經過此步驟後,基礎流動性 (baseL) 為零,但再投資流動性 (reinvestL) 不為零。
  1. 向資金池添加流動性,然後移除部分流動性。此步驟用於將範圍和總流動性控制在所需數量。
  • 刻度範圍是根據 currentSqrtP 選擇的。
  • 攻擊所需的目標流動性可以從刻度範圍推導出來,儘管相應的計算邏輯需要進一步研究。
  1. 在資金池中將 387.170 WETH 兌換為 0.06 frxETH。此步驟用於操控當前刻度,使得 nextTick == currentTick。具體而言,第 4 步的交易巧妙地欺騙了資金池,使其認為刻度 111,310 沒有被跨越。然而,實際上 currentSqrtP 確實大於刻度 111,310 的 sqrtP
  1. 在資金池中將 0.06 frxETH 兌換為 396.244 WETH。請注意,交易方向與上一步相反。在此步驟中,流動性被重複計算,從而使交易獲利並最終耗盡資金池。
  1. 歸還閃電貸,並收穫 6.364 WETH 和 1.117 frxETH。

如需深入了解,請參閱我們的 完整分析報告,其中包含更多計算細節和圖表。

總結

此事件的根本問題源於 KyberSwap 在再投資過程中錯誤的捨入,導致了不準確的刻度計算,並最終導致流動性被重複計算。這凸顯了 DeFi 協議中精確度損失問題的複雜性和隱蔽性,為整個社群帶來了嚴峻挑戰。

這次 2023 年的攻擊因其複雜性而顯得突出,具有極其細緻的計算,是許多考驗社群的精確度相關安全事件的典型案例。此外,在與有關當局進行廣泛談判後,攻擊者向公眾發布了一條挑釁性訊息,聲稱要求對該協議擁有完全控制權。

閱讀本系列的其他文章:

Best Security Auditor for Web3

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

BlockSec Audit