Back to Blog

zkLend 漏洞事後分析:解析 1,000 萬美元閃電貸攻擊的細節並釐清誤解

February 20, 2025
8 min read

2025 年 2 月 12 日,StarkNet 上的借貸協議 zkLend [1] 因其累加器機制遭到複雜操縱,導致約 1,000 萬美元的損失。攻擊者利用閃電貸和捨入漏洞人為抬高抵押品價值,進而從協議中借出其他資產獲利。

然而,目前從安全角度出發,針對該事件的詳細且準確的技術分析仍然缺失。儘管其他安全研究人員已提供了一些具備參考價值的分析,但針對該攻擊過程的解讀仍存在一些誤解。zkLend 隨後發布的官方事後剖析 [2] 提供了一個簡化後的描述,但缺乏深入的技術分析。在這篇部落格中,我們旨在提供全面的剖析以釐清該事件。

關鍵要點 (TL;DR)

  • 本次事件的根本原因源於以下三個問題的結合

    • 空市場初始化:允許任意資產存入。
    • zkLend 閃電貸中特殊的捐贈機制:導致累加器(作為動態調整用戶抵押品餘額的縮放因子,是一個全域變數)被操縱。
    • 因截斷導致的精度損失:與除法中常見的精度損失不同,此處分母最初為 1,隨後被放大至極大值,導致在銷毀份額代幣(share token)時產生低估。
  • 攻擊者並未從其他用戶存入的 wstETH 中獲利。相反,攻擊者利用漏洞操縱抵押品餘額,以少量 wstETH 作為初始資金,將抵押品餘額放大至超過 7,000 wstETH,從而實現了從市場借出其他資產的目標。

在接下來的章節中,我們將首先提供一些關於 zkLend 的關鍵背景資訊。隨後,我們將對上述問題及相關攻擊過程進行深入分析。

0x1 背景:了解 zkLend 的核心協議

zkLend 是 StarkNet 上的一個借貸項目,支持抵押借貸和閃電貸等常見借貸協議。讓我們深入了解這兩個協議的實作細節。

0x1.1 抵押借貸

抵押借貸是指用戶將特定資產存入協議作為抵押品,以借入其他資產的過程。抵押品的價值用於確定借款額度。需要注意的是,借貸協議通常不會直接儲存抵押資產的價值,而是透過以下公式計算:

collateral_balance = lending_accumulator * raw_balance

具體來說,lending_accumulator 是一個動態調整每個用戶抵押品價值的縮放因子,而 raw_balance 表示用戶在市場中持有的實際份額。raw_balance 是透過 lending_accumulatorcollateral_balance 中推導出來的。

此設計的目的是什麼? 它使協議能夠高效地管理抵押品價值,同時激勵用戶存入資產。透過將協議部分收益分配給抵押品提供者,lending_accumulator 會隨之增長,從而按比例同時放大所有用戶的抵押品價值。

0x1.2 zkLend 上的閃電貸

閃電貸是一種無需抵押的貸款,允許用戶在極短的時間內(通常在單筆交易內)從協議借出資產。如果借款人未能在交易結束前償還貸款或滿足特定條件,整筆交易將會回滾,貸款也不會執行。

在 zkLend 的閃電貸實作中,存在一個獨特的捐贈機制。具體而言,當用戶償還資產時,他們不僅可以歸還所需的最低金額,還可以額外捐贈資金。協議會追蹤這些捐贈的資金,並據此更新 lending_accumulator。此過程是透過 thesettle_extra_reserve_balance() 函數實現的。更新 lending_accumulator 的公式如下:

new_accumulator = (reserve_balance + totaldebt - amount_to_treasury) / ztoken_supply

  • reserve_balance:合約中持有的底層代幣(如 wstETH)總額,包括用戶捐贈的代幣。
  • totaldebt:所有借款用戶的債務總額。
  • amount_to_treasury:協議的總收入。
  • ztoken_supply:份額代幣(如 zwstETH)的總供應量。當用戶存入 wstETH 時,zkLend 的 ztoken 合約會鑄造等量的 zwstETH。

在理解上述 zkLend 核心協議後,我們現在將正式解釋攻擊者是如何透過操縱 lending_accumulatorraw_balance 變數來操縱其抵押資產的。

0x2 攻擊分析

攻擊者利用 zkLend 合約中的以下機制和漏洞來操縱抵押品價值:

  • 操縱 lending_accumulator
    • 空市場:攻擊發生前,zkLend 的 wstETH 市場處於空置狀態,為操作提供了絕佳條件。此外,zkLend 的 Market 合約允許任何人向空市場存入任意數量的資產。攻擊者存入了少量資產,從而顯著抬高了 lending_accumulator 的數值。
    • 捐贈機制:zkLend Market 合約的 flash_loan() 函數具有獨特的捐贈機制。具體而言,當用戶償還閃電貸時,Market 合約會計算多出的回撥資金並增加全域變數 lending_accumulator,從而放大合約中所有用戶的抵押品價值。
  • 操縱 raw_balance
    • 捨入行為:在銷毀份額代幣的除法運算中使用截斷處理,這導致在提款過程中用戶 raw_balance 的變動被低估。

透過對這兩個變數的操縱,攻擊者將抵押品餘額增加到了超過 7,000 個 wstETH,並從市場借走了其他資產獲利。

0x2.1 操縱 lending_accumulator 變數

0x2.1.1 空市場初始化

透過檢查攻擊前 Market 合約的交易記錄,可以看出攻擊者最初向 wstETH Market 合約存入了 1 wei 的 wstETH。審查該交易的內部呼叫可知,wstETH Market 合約中持有的 wstETH 為 0,且 zwstETH 的總供應量也為 0

因此,可以確認 zkLend 的 wstETH 市場在當時並無任何先前的存款或借款紀錄。reserve_balanceztoken_supply 均處於初始值 0,而 lending_accumulator 的初始值為 1。這種空市場場景為後續攻擊創造了條件,允許攻擊者以極少量的 wstETH 大幅放大 lending_accumulator

0x2.1.2 透過閃電貸操縱 lending_accumulator

接下來,在這筆交易中,攻擊者呼叫了 flash_loan() 函數,借入 1 wei 的 wstETH 並償還了 1000 wei 的 wstETH。多出的 999 wei 被視為捐贈並記錄在合約的 reserve_balance 中。

根據 lending_accumulator 的計算公式,這筆交易導致 lending_accumulator1 增加到了 851.0

0x2.1.3 重複執行 flash_loan()

攻擊者總共執行了 10 次 flash_loan() 呼叫,每次僅借入 1 wei 的 wstETH 但償還較大金額。結果,lending_accumulator 飆升到了天文數字 4,069,297,906,051,644,020 (4.069 × 10^18),這剛好與 wstETH 的小數精度對齊。

0x2.2 操縱 raw_balance 變數

在將 lending_accumulator 操縱至約 4.069 × 10^18 後,攻擊者呼叫了 Market 合約的 deposit() 函數,存入了 4.069297906051644020 wstETH。基於 lending_accumulator 的最新數值,攻擊合約的 raw_balance 變成了 2

0x2.2.1 第一次操縱 raw_balance 的交易

這筆交易中,攻擊者呼叫了攻擊合約的 callflashloandraaan() 函數。雖然該合約未開源,但根據內部呼叫追蹤可以推測,該函數包含一個執行以下操作的迴圈:

  • 存款:攻擊者向市場合約存入一定數量的 wstETH。
  • 提款:攻擊者提取特定數量的 wstETH。

代幣轉帳記錄分析

可以觀察到,攻擊者存入的 wstETH 金額始終是 lending_accumulator 的整數倍,例如 lending_accumulator 數值的 2 倍(即 8.13859)。

然而,提取出的 wstETH 金額卻是 lending_accumulator 數值的 1.5 倍(即 6.10394)。

透過計算,我們可以判定提出的 wstETH 金額超過了存入金額。為什麼會這樣?

捨入行為

透過檢視 deposit()withdraw() 方法的實作,我們可以看到這兩個方法分別涉及了 zwstETH 的鑄造與銷毀。其運作原理如下:

Market 合約中的 `mint()` 函數

Market 合約中的 `burn()` 函數

mint()burn() 過程中都包含了縮小處理(scale down logic)。該邏輯涉及進行向下取整(即將數值進行捨入)的整數除法,這是此次漏洞攻擊的核心。

當攻擊者銷毀特定數量的 zwstETH 時,會應用此縮小處理。由於 lending_accumulator 被操縱到極高(約 4.069297906051644020),導致儘管銷毀了超過 6 個 zwstETH,攻擊者的 raw_balance 卻僅減少了 1 個單位。

攻擊者的 raw_balance 變動總結如下表:

可以看出,在這筆交易中,攻擊者重複執行了 存款 - 提款 邏輯,利用了 withdraw() 過程中因精度損失導致的 raw_balance 差值計算誤差。最終,用戶的 raw_balance2 增加到了 3,額外獲得了一個單位。

0x2.2.2 後續攻擊過程

隨後的攻擊交易遵循了與第一次攻擊相同的模式:攻擊者透過循環執行 存款 - 提款 交易來獲取 wstETH。

獲取的 wstETH 被重新存入市場,進一步增加了 raw_balance,導致攻擊者的抵押品價值持續上升。

範例解釋

我們以這筆交易為例。

  • 總共進行了 30 次存款,每次存入 4.069 個 wstETH。
  • 總共進行了 30 次提款,每次提取 6.104 個 wstETH。
  • 根據計算,經過此循環,攻擊者成功獲取了 61.39 個 wstETH。

此外值得注意的是,在這些攻擊交易之間,還多次呼叫了 increase() 方法。這些方法用於將特定數量的 wstETH 從攻擊者的帳戶轉移到攻擊合約,從而為後續向 Market 合約的存款提供資金。

這些操作提升了 raw_balance 的數值,使攻擊者能持續增加抵押品價值。最終,攻擊者的 raw_balance 達到了 1,724,價值約為 7,015.4wstETH,這使其足以從市場中借出其他資產

0x3 獲利分析

0x3.1 借出其他類型的基金

在操縱抵押品價值後,攻擊者從市場中借出了其他類型的基金並進行了以下交易(節選):

0x3.2 將借出的資金轉帳至 Layer 1

透過檢查攻擊者合約的橋接交易,可以發現攻擊者已將部分借出的資金轉移至 Layer 1。

0x4 結論

總結來說,此次針對 zkLend 協議的攻擊為去中心化借貸協議的設計與安全性帶來了幾項重要啟示:

  • 市場初始化與資產存入條件: 市場啟動時的空白狀態允許攻擊者存入少量 wstETH 並操縱 lending_accumulator,從而獲得漏洞利用的槓桿。確保足夠的流動性基礎,或在市場初期限制資產捐贈,有助於防止類似攻擊。
  • 適當累加器機制的重要性: 攻擊者利用 flash_loan() 函數中的捐贈機制來操縱 lending_accumulator,導致所有用戶的抵押品價值被放大。採用基於累加器機制的協議應針對縮放因子的簡單操縱設立防護措施。
  • 捨入行為與精度損失: zwstETH 代幣銷毀過程中的捨入問題導致了精度損失和對 raw_balance 的低估,使得攻擊者能夠操縱 raw_balance。協議應使用更高的精度或進行驗證檢查,以防止此類利用手法。

這再次強調了針對初始化過程和運行狀態的及時通知,以及進行主動式威脅預防以減輕潛在損失的重要性。

參考資料

[1] https://zklend.com/

[2] zkLend 安全事件事後剖析:https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view