rounded引言

隨著 DeFi 生態系統的迅速發展,Compound Finance V2 作為該領域的先驅者之一,憑藉其創新的借貸模式吸引了大量用戶。然而,任何複雜的分佈式應用都面臨著潛在的安全威脅,尤其是涉及到價值數百萬甚至上億美金的資金流動時。因此,對 Compound Finance V2 及其分叉項目進行全面且細緻的安全審計顯得尤為重要。本手冊旨在為開發者、安全研究員以及 DeFi 愛好者提供一份詳盡的安全審計指南,幫助大家更有效地識別和防範潛在的風險。

1. 項目背景概述

Compound Finance V2 是一個基於以太坊區塊鏈構建的開放式借貸平台,允許用戶存入各種 ERC-20 底層代幣並從中賺取利息,同時也允許以支付利息的形式借用市場中的代幣。通過引入“利率市場”的概念,它實現了去中心化的資金池管理和自動化的利率調整機制。

2. 項目架構分析

Compound Finance V2 的核心架構組件包括:

  • Comptroller:控制整個系統邏輯,如利率計算、賬戶狀態維護等。

  • cToken:實現 ERC-20 標準的自定義代幣,代表用戶在系統中的權益。

  • InterestRateModel:計算存款和借款利率的模型。

  • PriceOracle: 提供資產價格的預言機。

  • Governance:負責社區治理相關的功能。

2.1 Comptroller

Comptroller 合約是 Compound Finance V2 的中樞神經系統,它負責協調各個 cToken 實例的行為。主要職責有:

  • 管理市場列表,確定哪些市場是活躍的。

  • 執行跨市場操作的各類檢查,如用戶的頭寸健康度檢查等。

  • 設置和更新全局參數,如借款限額、抵押因子、清算閾值等。

2.2 cToken

每個支持的 ERC-20 代幣都有一個對應的 cToken 實例(即 CErc20 / CEther 合約),用於處理該代幣所有與項目的交互操作。每個 cToken 除了實現了基本的代幣轉帳功能外,還添加了一些特定於 Compound 的功能,如借貸、累積利息和分配獎勵。所以我們可以將 cToken 看作是用戶在 Compound 上存入資產的憑證和用戶進行借貸操作的入口。

當用戶將底層的資產代幣存入合約後,即可鑄造對應的 cToken 代幣,cToken 與標的資產的兌換比例按照如下公式計算:

注意:borrows 表示借款额,cash 表示資金池餘額,reserves 表示儲備金。借款利率由使用率決定,存款利率由借款利率決定。

用戶一般通過與不同的 cToken 合約交互來在不同的市場中進行代幣的借貸操作:

2.3 InterestRateModel

InterestRateModel 合約定義了計算利率的方法。不同的市場可能會使用不同類型的利率模型,以適應各自的風險偏好和流動性需求。

Compound V2 的市場中使用的利率模型主要有兩種,一種是直線型,一種是拐點型。

直線型模型的借款利率計算公式如下:

資金使用率的計算公式如下:

存款利率則隨著借款利率線性變化:

使用率逐漸升高則意味著資金池裡的錢在逐漸減少,當達到一定峰值時可能會導致用戶無法正常存款和借款。為盡量避免這種情況,Compound 推出了第二種利率模型 —— 拐點型。

拐點型的借款利率計算公式如下:

當使用率達到一定的峰值時,會瞬間大幅提高借款利率和存款利率,激勵用戶多存款少借款,以此將使用率控制在合適的範圍,這個峰值也被稱為拐點(一般是利用率達到 80% 時)。

2.4 PriceOracle

PriceOracle 合約負責獲取外部市場價格信息,並將其轉換為系統內部使用的數值,這對於準確計算用戶的頭寸價值至關重要。

2.5 治理機制與激勵模型

Compound 引入了一種獨特的治理機制,允許持有治理代幣(COMP) 的用戶參與重要決策的投票,如更改某些參數或添加新的資產類型。通過發行治理代幣(COMP),Compound 激勵用戶積極參與平台活動,並為貢獻者提供獎勵。詳細內容可參考 Compound 官方文檔和代碼倉庫。(https://docs.compound.finance/v2/; https://github.com/compound-finance/compound-protocol)

3. 交互流程

接下來,我們通過簡單示例來說明用戶在 Compound Finance V2 上進行交互的大致過程:

3.1 存款和贖回流程

如果用戶 Alice 需要將 1 個 WBTC 存入 Compound,那麼他將調用 cWBTC 合約的 mint 函數來進行存款。該合約繼承了 cToken 合約,會先通過 mintInternal 函數內部調用 accrueInterest 函數來更新借款和存款利率,之後調用 mintFresh 進行具體的鑄造操作。

mintFresh 函數會外部調用 Comptroller 合約的 mintAllowed 函數來檢查當前市場是否允許存款,然後將用戶的 1 個 WBTC 通過 doTransferIn 函數轉入合約,再根據當時最新的兌換率為用戶鑄造相應數量的 cToken 代幣(假設當前最新的兌換率是 0.1,那麼 Alice 將收到 10 個 cWBTC 代幣)。

如果 Alice 未來決定贖回存款,她可以通過調用 redeem 函數將 cWBTC 兌換回 WBTC,兌換率可能已經改變(假設為 0.15),這意味著 Alice 能夠贖回 1.5 個 WBTC,其中 0.5 個 WBTC 為利息收入。

3.2 借款和還款流程

Alice 首先需要調用 Comptroller 合約的 enterMarkets 函數將她的 cWBTC 設置為可作為抵押品的狀態,之後才可以進行借款。

假設 Alice 選擇借出 70 個 USDC,由於 WBTC 的抵押因子為 0.75,Alice 最多可以借出相當於 75% 的 WBTC 價值資產,所以這不會超過她的最大借款額度。

注意:為了避免被清算的風險,Alice 應該保留一定的緩衝空間而不是完全用尽她的借款額度。

Alice 調用 cUSDC 合約的 borrow 函數,其會先通過 borrowInternal 函數內部調用 accrueInterest 函數來更新借款和存款利率,之後調用 borrowFresh 進行具體的借款操作。

在通過 Comptroller 合約的 borrowAllowed 函數進行用戶的頭寸價值檢查後,先進行借款數據的記帳,之後通過 doTransferOut 函數將代幣轉出給用戶。

若 Alice 需要還款,可以通過調用 cUSDC 合約的 repayBorrow 函數自行還款,或者讓其他人調用 repayBorrowBehalf 函數來代還款。

3.3 清算流程

如果 WBTC 的價格大幅下跌,使得 Alice 的抵押品價值低於其借款額度的 75%,則 Alice 的貸款頭寸將處於被清算狀態。

外部清算人(例如 Bob)可以調用 cUSDC 合約中的清算函數 liquidateBorrow 來幫助 Alice 清償部分債務。其會先通過 liquidateBorrowInternal 函數同時更新 cUSDC 與還款用的抵押品 cToken 的利率,之後調用 liquidateBorrowFresh 進行具體的清算操作。

在通過 Comptroller 合約的 liquidateBorrowAllowed 函數進行是否允許清算的檢查後,會先調用 repayBorrowFresh 函數將 USDC 轉入合約進行還款,並更新被清算人的借款數據。接著調用 Comptroller 合約的 liquidateCalculateSeizeTokens 函數根據清算的價值來計算 Bob 可以拿到 Alice 相應價值的抵押品數量,最後通過指定抵押品市場的 cToken 合約(例如 cWBTC)的 seize 函數來為 Bob 和 Alice 轉移 cToken。

打開此鏈接可查看上圖的高清版,點擊閱讀原文也可直接跳轉:https://www.figma.com/board/POkJlvKlWWc7jSccYMddet/Compound-V2?node-id=0-1&node-type=canvas。

Bob 為 Alice 清償部分貸款(例如 20 USDC),並因此獲得 Alice 相應價值的抵押品(如 WBTC),同時 Bob 還能額外獲得一筆清算激勵(假設為 5%)。最終結果是 Bob 收到了價值 21 個 USDC 的 WBTC(20 USDC 的貸款 + 1 USDC 的清算激勵)。

4. 安全漏洞 Checklist

4.1 空市場導致的舍入漏洞

如果 cToken 是一個空市場的情況(即沒有用戶在市場中進行借貸),由於 exchangeRateStoredInternal 函數中 exchangeRate 的值依賴於合約對應的底層資產代幣的數量,所以可以通過向 cToken 合約轉入大量的底層資產代幣來操縱 cToken 的價格。

因此,可以用少量的 cToken 借出大量的其他代幣,之後再調用 cToken 的 redeemUnderlying 函數來提取底層資產代幣。在計算贖回時,需要扣除的 cToken 數量會由於除法的向下舍入導致結果遠少於預期(幾乎只有一半)。

假設此時持有的 cToken 的數量是 2(同時也是總的 totalSupply),而 exchangeRate 在經過操控後被拉高為 25,015,031,908,500,000,000,000,000,000,需要贖回的底層資產代幣數量為 50,030,063,815。那么預期應該扣除的 cToken 數量應該為:

而實際計算出來的 cToken 數量卻為:

因此,最後只需要清算極少數的 cToken 就可以獲得從其他市場中借出的大量資產代幣。

可以參考由於該漏洞導致的 Compound 分叉項目 Hundred Finance 被黑的交易:https://optimistic.etherscan.io/tx/0x6e9ebcdebbabda04fa9f2e3bc21ea8b2e4fb4bf4f4670cb8483e2f0b2604f451

審計要點:在審計時,需要關注兌換率的計算方式是否容易被操控以及舍入的方式是否恰當,同時可以建議項目團隊在新的市場創建後立刻鑄造小額的 cToken,以防止市場為空進而被操控。

4.2 ERC677 / ERC777 代幣導致的重入漏洞

ERC677 / ERC777 是 ERC20 合約的一個擴展,兼容 ERC20 代幣的協議標準。這些代幣允許在轉帳過程中,如果接收地址是合約則會觸發接收地址的回調函數(如 transferAndCall 或 tokensReceived)。

在舊版本的 Compound Finance V2 代碼中,當用戶在 cToken 市場中進行借款時,會先將被借的代幣轉出,之後再進行借款數據的記帳。

假如用戶借出的代幣是帶有回調功能的 ERC677 / ERC777 代幣的話,那麼可以構造接收代幣的惡意合約來通過回調函數重入 borrow 函數中進行再次借款,由於上一次借款時用戶的借款數據還未被記帳,所以此時可以成功通過賬戶的健康系數檢查來再次借出代幣。

可以參考由於該漏洞導致的 Compound 分叉項目 Hundred Finance 被黑的交易: https://blockscout.com/xdai/mainnet/tx/0x534b84f657883ddc1b66a314e8b392feb35024afdec61dfe8e7c510cfac1a098

審計要點:最新版本的 Compound V2 代碼中已經修復了借款邏輯,改為先記錄借款的數據再轉出被借的代幣。在審計中,需要關注借貸功能的相關代碼是否符合 CEI(Checks-Effects-Interactions) 規範,並且需要考慮具有回調功能的代幣造成的影響。

4.3 不恰當的預言機機制導致的價格操控風險

由於 Compound Finance 采用超額抵押貸款的模式,用戶能借出的代幣數量取決於抵押品的價值是否足夠。

因此,如果項目在計算抵押品價值時所采用的預言機的喂價機制容易被操控,則很容易借出超預期的代幣。

舉個例子,在 Compound 分叉項目 Lodestar Finance 被黑的事件中,預言機獲取抵押品 plvGLP 代幣價格的方式是先將 plvGLP 合約中 plsGLP 代幣的數量(totalAssets) 除以 plvGLP 的總供應量(totalSupply) 計算出兌換率,再將兌換率乘上 GLP 代幣的價格計算出 plvGLP 代幣的價格。

而 plvGLP 有一個捐贈的功能,允許用戶捐贈 sGLP 為 plvGLP 代幣合約鑄造相應的 plsGLP 代幣。

所以攻擊者可以先利用閃電貸在 Lodestar Finance 市場中創建大量 plvGLP 抵押品頭寸,之後在 GMX 上利用閃電貸大量鑄造 sGLP,再通過 donate 函數為 plvGLP 合約鑄造 plsGLP 代幣以增加 totalAssets 的值。隨著總資產的增加,plvGLP 的匯率會變大,導致 plvGLP 代幣的價格瞬時急速上漲,從而可以在市場上借出超出預期的其他代幣。

可以參考 Lodestar Finance 被黑的交易:https://arbiscan.io/tx/0xc523c6307b025ebd9aef155ba792d1ba18d5d83f97c7a846f267d3d9a3004e8c

此外還需注意的是,Compound Finance 或其分叉項目也會采用鏈下預言機例如 ChainLink 或者 CoinBase 來獲取抵押品的價格。如果遇到市場劇烈波動的情況,可能會導致鏈下價格與鏈上出現價差而危害項目的資金安全。

例如 LUNA 代幣的價格由於市場原因而急速暴跌,而 Compound Finance 的分叉協議 Venus Protocol 和 Blizz Finance 都使用 Chainlink 預言機作為喂價來源來計算抵押品的價值,其中對 LUNA 代幣的最低價格(minAnswer) 進行了硬編碼,其值為 0.10 美元。

當 LUNA 代幣的價格跌破 0.1 美元時(例如 0.001 美元),任何人都可以按市場價格購買大量 LUNA,並將其作為抵押品(價值 0.10 美元)從平台借出其他資產。

審計要點:在審計時,需要關注計算抵押品價值時采用的預言機喂價機制是否容易被外部操控,可以建議項目方采用多種價格來源進行綜合評估,以規避單一價格來源造成的風險。

4.4 多入口點代幣導致的匯率操控風險

在 Compound 的代碼中有一個名為 sweepToken 的函數,其作用是為了讓不小心將代幣轉入到合約的用戶能夠取出這些代幣。舊版本的代碼如下,這個函數有一個重要的安全檢查:傳入的 token 參數不能是合約的底層資產代幣。

然而,假如某個 cToken 市場的底層資產代幣存在多個入口點合約(通過多個合約地址能訪問同一底層餘額,外部交互影響所有入口點的餘額,這是一種早期的類似代理的模式),攻擊者則可以調用 sweepToken 函數通過傳入與 underlying 不同的入口點合約,將合約中的底層資產代幣轉出。

下面以 TUSD 為例,其擁有兩個入口點合約,輔助入口點合約 0x8dd5fbce 會將任何的調用(例如 transfer 或者 balanceOf)轉發到主合約,這意味著與其中任何一個合約的交互會影響兩個合約中的餘額數據(即兩個不同的合約共用相同的餘額數據)。

此時假設市場中設置的底層代幣地址是 TUSD 的主合約地址,那麼我們可以在調用 sweepToken 函數時將輔助入口點合約地址 0x8dd5fbce 作為傳入的 token 參數,則可以成功通過檢查 address(token) != underlying,之後合約會將其中全部的底層資產代幣 TUSD 轉移到管理者地址。

而 TUSD / cTUSD 的兌換率會受到 cTUSD 合約中底層資產代幣 TUSD 數量的影響,當 TUSD 被全部轉移到管理者地址後,TUSD / cTUSD 的匯率會瞬間暴降。此時攻擊者可以以極低的兌換率去清算其他用戶或者在借款之後償還少於預期的代幣數量來獲利。

值得一提的是,Compound V2 的最新版本代碼中對 sweepToken 函數添加了權限驗證,保證只能由管理者角色來調用該合約,並且已經移除了所有存在多入口點代幣的市場。

審計要點:在審計時,對於轉移合約內代幣的功能,需要考慮到多入口點代幣存在的場景對項目造成的影響,可以建議項目方不采用多入口點代幣或者驗證代幣轉移前後合約中的底層資產代幣數量是否會有變化,並對相關的功能做好權限的檢查。

4.5 新舊版本合約代碼的兼容性問題

如果在 Compound Finance V2 分叉項目中,某個核心合約的代碼分叉的是新版本的 Compound Finance V2 代碼,而與其交互的某個其他合約采用的卻是舊的代碼版本,那麼可能會出現兼容性的问题。

例如舊版本的 cToken 使用的 InterestRateModel 合約中獲取借款利率的函數 getBorrowRate 的返回值是兩個 uint 類型的值,而在新版本的 InterestRateModel 函數中,getBorrowRate 函數只會返回一個 uint 類型的值。

但是在 Compound Finance V2 分叉項目 Percent Finance 中,項目方使用的是舊版本的 cToken 合約代碼,而 InterestRateModel 合約卻是采用的新版本,這就導致了 cToken 中的 accrueInterest 函數調用 getBorrowRate 函數時會失敗。而 accrueInterest 函數在提現和借貸中都有使用到,最終使得提現和借貸功能均無法正常進行,合約中的資金被徹底鎖住。

審計要點:在審計時,需要關注更新的代碼中的合約接口、狀態變量、函數簽名和事件的變更是否會破壞現有系統的正常運行,確保所有合約代碼版本更新的一致性或者保證更新後的代碼能夠兼容舊版本的代碼。

4.6 多鏈部署導致的硬編碼問題

在 Compound Finance V2 的代碼中,常量 blocksPerYear 代表每年產出區塊的預估數量,其值在利率模型合約中被硬編碼為 2102400 ,這是因為以太坊的平均出塊時間為 15 秒。

然而不同鏈的區塊時間不一定相同,同樣全年產出的大致區塊數量也不一定是相同的。如果某個 Compound 的分叉項目在其他鏈上部署,但是卻沒有根據不同鏈的情況修改硬編碼的值,那麼可能會造成利率最後計算的結果超出預期。這是因為 blocksPerYear 的值會影響到 baseRatePerBlock 和 multiplierPerBlock 的值,而 baseRatePerBlock 和 multiplierPerBlock 最終會影響到借款利率。

例如 BSC 鏈的出塊時間是 3 秒,那麼全年預估的出塊數量(blocksPerYear) 應該為 10512000。如果在部署前沒有修改 blocksPerYear 的值,那麼會導致最後計算出來的借款利率比預期高出五倍。

審計要點:在審計時,要關注項目合約中硬編碼的常量或變量在不同鏈的特性下是否會造成非預期的結果,建議項目方根據不同鏈的情況來正確地修改其值。

其他

除了上面提到的這些主要關注的問題,Compound V2 的分叉項目通常會根據項目團隊的設計來修改部分業務邏輯,例如添加與外部第三方協議進行交互的代碼。這需要在審計時根據其具體的業務邏輯和設計需求去評估是否會對 Compound Finance V2 本身的核心借貸模型以及項目造成影響。

寫在最後

希望這份 Compound Finance V2 及其 Fork 項目的安全審計手冊能幫助大家在審計時更好地理解和評估此類複雜系統的安全性,隨著技術的迭代更新,本手冊也會隨之更新和完善。

參考:

[1] https://github.com/YAcademy-Residents/defi-fork-bugs

[2] https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2

[3] https://github.com/code-423n4/2023-05-venus-findings/issues/559

[4] https://learnblockchain.cn/article/2593

[5] https://github.com/compound-finance/compound-protocol

作者 | 九九

編輯 | Liz