Back to Blog

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

Code Auditing
October 10, 2021
8 min read

由 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 函數的調用鏈為:mintmintInternalmintFresh

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 用於調整市場的 supplySpeedcompSpeeds[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) 的檢查,用於初始化新市場indexblock。然而,由於我們是在重新啟用既有市場(而非新市場)的 COMP 分發,因此第 1090 和 1091 行的初始化語句將不會被執行(因為 compSupplyState[address(cToken)].block 不為零)。

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

漏洞 1 的影響

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

supplierIndexcompInitialIndex。然而,由於該漏洞,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

Best Security Auditor for Web3

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

BlockSec Audit