返回部落格

【蝴蝶效應】由錯誤修正(Bugfix)引發的複合型資安事件

Code Auditing
2021年10月10日
閱讀約 8 分鐘

由 BlockSec 團隊 (@BlockSecTeam) 提供

在過去的一週裡,Compound 協議出現了一個漏洞,該漏洞會「意外地」向用戶發送大量的 COMP 代幣。該漏洞(本文中的漏洞 2)的起因,源於先前發現的另一個漏洞(本文中的漏洞 1)修復不當所致。

在本文中,我們將詳細闡述第一個漏洞的根本原因,以及為什麼對第一個漏洞的修復會導致第二個漏洞。

背景

Compound 協議基於 Compound 白皮書構建。透過 cToken 合約,區塊鏈上的帳戶可以提供資本(以太幣或 ERC-20 代幣)以換取 cToken,或從協議中借出資產(並持有其他資產作為抵押品)。Compound 的 cToken 合約會追蹤這些餘額,並透過演算法為借款人設定利率。

為了激勵用戶,為 Compound 提供流動性(供應資本)的用戶可以獲得利息。具體來說,用戶向 Compound 提供資產(例如以太幣或其他 ERC-20 代幣)並獲得相應的 cToken。當 cToken 被歸還給 Compound 時,如果用戶在 Compound 中沒有債務,底層資產(以太幣或 ERC-20 代幣)及其產生的利息將會退還給用戶。例如,如果使用者擁有 1000 以太幣,他/她可以透過 cEth.mint(1000) 將資產存入 Compound 以獲得 cToken。

cToken 代表了鎖定在 Compound 中的底層資產。用戶可以進一步將 cToken 作為抵押品來借出其他資產。例如,用戶可以透過 ceth.mint(1000) 存入 1000 以太幣,然後利用獲得的 cToken 透過 cDai.borrow(x) 借出價值 75 以太幣的 x Dai(過度抵押——此數值取決於抵押係數)。

核心邏輯在 Comptroller 合約中實現。它維護用戶的狀態,例如用戶在 Compound 中存入了多少代幣、借出了多少代幣,以及該用戶是否可以借入更多代幣。此過程中所調用的函數包括 getHypotheticalAccountLiquidityInternal()、borrowAllowed()、mintAllowed() 等。

Compound 還有一個治理代幣,稱為 COMP。COMP 代幣可用於投票表決提案。此外,COMP 代幣還可以在交易所進行交易。目前,COMP 的價格約為 300 美元。

漏洞 1

2021 年 9 月 31 日(註:原文如此),Compound DAO 提出了一個新提案(提案 62),旨在修復 Comptroller 中的一個漏洞。

該漏洞與 CompSpeed 有關,它代表每個區塊可分配給用戶的 COMP 代幣數量。

mint 函數的流程

下文中,我們將使用 mint 函數來描述此漏洞的成因。mint 函數的調用鏈為:mint → mintInternal → mintFresh。

在 mintFresh 函數中,它會調用 mintAllowed,然後更新用戶的 cToken 餘額。

在 mintAllowed 函數中,它首先調用 updateCompSupplyIndex,然後調用 distributeSupplierComp 以 1) 更新市場的 compSupplyState,以及 2) 將 COMP 代幣分配給用戶。

updateCompSupplyIndex

updateCompSupplyIndex 函數將更新每個市場的狀態,主要是 compSupplyState[cToken]。

在 CompMarketState 結構中,它記錄了此更新的區塊編號(block),以及將影響應分配給用戶(持有 cToken 的用戶)的 COMP 代幣數量的獎勵索引(index)。

每個代幣的獎勵索引(index)是什麼? 這是一個隨時間累積的值(如下公式所示)。

這顯示了應分配給用戶的 COMP 數量(針對用戶持有的每個 cToken)。

distributeSupplierComp

另一個函數 distributeSupplierComp 負責記錄應分配給用戶(供應方)的 COMP 代幣數量,記錄在 compAccrued[supplier] 中。

具體來說,它會更新 compSupplyState 中的全域獎勵索引(在 updateCompSupplyIndex 函數中)。然後在 distributeSupplierComp 函數中,supplyIndex 記錄當前的獎勵索引,而 supplierIndex 顯示用戶(供應方)最後一次的獎勵索引。差值 (supplyIndex - supplierIndex) * 用戶的 cToken 餘額 即為應分配給用戶的 COMP 代幣數量。

漏洞 1 的成因

還有另一個函數 setCompSpeed 用於調整市場的 supplySpeed(compSpeeds[address[cToken]])。

這是因為如果我們將市場的 CompSpeed 設置為零,意味著 COMP 代幣將不會分配給該市場的用戶。因此,如果我們想先禁用某個市場的 COMP 分配,隨後再重新啟用它,我們可以遵循以下步驟:

  • 步驟 I:將 CompSpeed[cToken] 設置為零,以禁用 COMP 代幣的分發。
  • 步驟 II:調用 setCompSpeed 函數,將 CompSpeed[cToken] 設置為非零值。

步驟 I:對於在步驟 I 中已禁用 COMP 分發的市場(supplySpeed == 0),其區塊編號並非零,因為區塊是在 updateCompSupplyIndex 中持續更新的(else if (deltaBlocks > 0))。

步驟 II:當執行步驟 II 中的操作時,setCompSpeedInternal 函數將執行 else if (compSpeed != 0) 語句(第 1083 行)。然後,在第 1088 到 1093 行之間,有一個 if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) 的檢查,用於初始化新市場的 index 和 block。然而,由於我們是在重新啟用既有市場(而非新市場)的 COMP 分發,因此第 1090 和 1091 行的初始化語句將不會被執行(因為 compSupplyState[address(cToken)].block 不為零)。

總之,對於當前已禁用的市場,index 為零。然而,block 並不為零。這意味著當我們透過調用 setCompSpeed 將 CompSpeed[cToken] 設置為非零值來重新啟用市場時,索引值將不會被重新初始化為 CompInitialIndex (1e36)(第 1090 和 1091 行未執行)。

漏洞 1 的影響

我們進一步深入研究負責分配 COMP 代幣的 distributeSupplierComp 函數。

supplierIndex 為 compInitialIndex。然而,由於該漏洞,supplyIndex 仍為零,這會導致 Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36) 產生下溢。

漏洞 2:因修復漏洞 1 而引入

為了修復該漏洞,項目方更改了代碼邏輯。具體來說,在初始化新市場時,它會立即將 index 初始化為 compInitialIndex。

由於全域獎勵索引(index)已被初始化為 compInitialIndex,用戶獎勵索引也應初始化為該值。讓我們看一下 distributeSupplierComp 函數。

即使 supplierIndex == 0,第 1234 行的 if 條件也無法滿足,因為 supplyIndex 等於(而非大於)compInitialIndex (1e36)。這導致 supplierIndex 沒有被正確初始化為 compInitialIndex(其值為 0)。然後,deltaIndex (supplyIndex - supplierIndex) 將會成為 compInitialIndex,而不是零。如果用戶的 cToken 餘額不為零,supplierTokens 將變成一個極大的數值。

總之,如果用戶恰好在漏洞 1 修復之前執行了 mint 操作,那麼他/她已經擁有 cToken,且 supplierIndex 會變成 0(因為 COMP 代幣已被分配)。隨後在漏洞 1 修復後(這引入了漏洞 2),當該用戶再次調用 mint 函數時,他/她可以獲得大量的 COMP 代幣(1e36 * cToken.balanceOf(user))。

實際案例

我們在下文中列出了受影響的市場:

0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI

對於該用戶(0xa7b95d2a2d10028cc4450e453151181cbcac74fc),其在本次交易中獲得了 4,466.542459954989867175 COMP 代幣(交易位址:0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308)。

進一步調試該交易顯示,由於漏洞 2,deltaIndex 為 1e36,且該用戶當時恰好持有 cToken。

漏洞 2 的修復

漏洞 2 的修復很簡單。它更改了 distributeSupplierComp 函數中的 if 條件。

教訓

  • 這是一個因修復另一個漏洞而導致的漏洞。如何徹底審查高知名度項目的代碼更改仍然是一個懸而未決的問題。
  • DAO 可以消除中心化風險。然而,它也使得安全事件的響應過程變得緩慢。
  • 高知名度的 DeFi 項目可以借鑒傳統軟體程式中的良好安全實踐,例如部署具有持續測試流程的高效模糊測試 (fuzzing) 系統。

關於 BlockSec

BlockSec 是一家開創性的區塊鏈安全公司,於 2021 年由一群全球傑出的安全專家創立。公司致力於增強新興 Web3 世界的安全性和易用性,以促進其大規模採用。為此,BlockSec 提供智慧合約和 EVM 鏈的安全審計服務、用於安全開發與主動防禦威脅的 Phalcon 平台、用於資金追蹤與調查的 MetaSleuth 平台,以及協助 Web3 開發者在加密世界中高效航行的 MetaSuites 瀏覽器擴充功能。

迄今為止,公司已為包括 MetaMask、Uniswap Foundation、Compound、Forta 和 PancakeSwap 在內的 300 多家知名客戶提供服務,並獲得了來自 Matrix Partners、Vitalbridge Capital 和分佈式資本 (Fenbushi Capital) 等頂尖投資機構的兩輪數千萬美元融資。

官方網站:https://blocksec.com/

官方 Twitter 帳號:https://twitter.com/BlockSecTeam

訂閱最新動態
Bitget 3.875億美元鏈下違規事件:超越金鑰與合約的範疇
Security Insights

Bitget 3.875億美元鏈下違規事件:超越金鑰與合約的範疇

2026年9月24日,攻擊者利用第三方安全產品漏洞,獲取內部憑證並偽造提款指令,導致約3.875億美元從Bitget部分營運錢包(涵蓋Ethereum、其他EVM網絡、XRP Ledger、Zcash及TRON)轉出;私鑰及冷錢包未受影響。本文深入梳理事件路徑與資金流向,分析快速轉換為原生資產及生態恢復應對,提出機構縱深防禦框架,並說明授權區塊鏈滲透測試如何驗證跨層假設。

約1130萬美元損失:Multicall Router、Nostra|BlockSec Weekly
Security Insights

約1130萬美元損失:Multicall Router、Nostra|BlockSec Weekly

本報告涵蓋2026/09/14至2026/09/20期間,探討以太坊與Starknet上兩起合計損失約1130萬美元的安全事件。較大一起中,多重呼叫路由器將自身位址接受為分派目標,導致巢狀呼叫以路由器自身已授權身分而非外部呼叫者身分進入Safe錢包的Gateway模組,約2900枚`aEthrsETH`被轉出至攻擊者建立的Uniswap v4池。在Starknet上,Nostra預言機整合僅要求最少一個聚合來源,當三個設定價格來源中僅兩個達成聚合時,操縱的薄池報價與正常報價平均後將`NSTR`估值約49.52美元,支撐約350萬美元以高估抵押品借貸。

损失約3.2億美元:Liquid Network與Symbiosis遭攻擊事件 | BlockSec
Security Insights

损失約3.2億美元:Liquid Network與Symbiosis遭攻擊事件 | BlockSec

本報告涵蓋2026/09/07至2026/09/13,探討兩起造成約3.2億美元損失的安全事件,包括前份報告未提及、發生於2026/09/06的Liquid Network漏洞事件。較大者為該Liquid Network漏洞:Elements中的rangeproof驗證快取藉由雜湊四個欄位(其中兩個長度可變)串接而成金鑰,且未標示欄位邊界,導致某輸出的驗證結果被錯誤套用至另一個未經檢驗證明的輸出,使攻擊者得以憑空創造4,000枚無擔保L-BTC,並將幾乎全數兌回比特幣。在橫跨BNB Smart Chain、Ethereum與Rootstock的Symbiosis跨鏈橋比特幣路徑中,讀取比特幣存款的鏈下程式碼從存款人可自行控制的欄位取得其身份,並在未檢查手續費是否為負值的情況下自存款中扣除,使330聰的存款鑄造出`46,116,860,184.27388234 syBTC`;其須賣出所經之資金池僅有11.26 syBTC,致使流動性提供者與用戶損失約9.97 BTC(約77萬美元)。

Web3 最佳安全審計方

在上線之前驗證設計、程式碼與業務邏輯,對標業內最高安全標準。

BlockSec 審計