撰文:BlockSec
原標題:《似曾相識燕歸來:Vee Finance 安全事件分析》
北京時間2021 年9 月21 日,Avalanche 鏈上的借貸項目Vee Finance 的合約遭受攻擊,造成數千萬美金的損失(8804.7ETH 和213.93 BTC)。
通過對相關合約和攻擊交易的分析,我們發現這是一起由於該項目提供的「槓桿借貸」功能不完善導致的價格操縱攻擊(即price manipulation attack,我們的 論文 對此做了系統的闡述)。以下我們將對合約漏洞和攻擊流程進行完整的分析。
相關背景簡介
Vee Finance 是基於Compound 的一個借貸項目,在Compound 的基礎上做了一些優化和功能更新。而此次被攻擊的函數正是在這些新功能的一部分。
首先需要知道的是,Compound 是一個借貸平台,通過超額抵押進行借貸。 Vee 在此基礎上提供了一個和攻擊有關的新功能:槓桿借貸。
超額借貸的意義在於,抵押價值100 美元的Ether 可以藉出80 美元的USDC,當借貸者無法償還的時候可以沒收抵押物以償還債務,而只要市場波動在一定範圍內,抵押品的總價值一定大於貸出的價值,這樣就避免了現實世界中藉貸需要依賴信譽這個問題。
但是這樣顯然不夠「刺激」。 Vee 提供的槓桿借貸原理很簡單:使用者可以指定槓桿倍數,假如槓桿為3,那麼抵押價值100 美元的Ether 可以藉出80*3=240 美元的USDC。
當然,Vee 不會讓用戶直接拿走這筆錢(這樣貸出價值就大於抵押品價值,用戶可以直接違約),而是允許用戶做一筆交易。
這個邏輯在合約的borrowAndCall 函數中實現:
函數實現首先調用borrowLeverageInternal 函數進行槓桿借貸,槓桿比率由參數leverage 指定。然後調用callOrderProxyInternal 執行用戶指定的交易(Order)。
下面首先來看borrowLeverageInternal (內部會調用邏輯實現borrowFresh)的實現。大部分代碼與Compound 相同,只有處理leverage 的時候存在不同:
當用戶指定的槓桿為0 時,執行正常的借貸邏輯,把資金直接轉給借貸人;當存在槓桿時,將槓桿記錄在狀態變量中,然後調用doDeposit 函數將資金轉移到某個固定的位置(防止用戶直接取走)。
需要注意的是,borrowLeverageInternal 的實現除了上述對leverage 的處理之外,和原版Compound 並沒有任何不同。也就是說,對於這一筆槓桿借貸,系統只會記錄單倍槓桿的借貸額。也就是說,雖然用戶用100 美元抵押借出了240 美元,但係統記錄的用戶借款仍然只有80 美元。
doDeposit 函數的實現很簡單,將資金轉入orderProxy 合約中。
callOrderProxyInternal 函數的實現也很簡單,就是將用戶指定的order 發給orderProxy 進行執行。
總結一下:用戶可以進行抵押借貸。當使用抵押借貸時,資金不會轉給用戶,而是轉給Vee 控制的orderProxy 合約,後者會執行一個用戶指定的訂單。
漏洞成因分析
我們回顧一下一個正常的槓桿交易訂單是怎樣操作的:
- 假設我們擁有1 個Ether,想用槓桿來增加獲利,Vee 正好提供了槓桿交易的功能。
- 我們抵押了1 個Ether,根據超額抵押原則,能藉出0.5 個Ether;再加上三倍槓桿,總共可以藉出1.5 個Ether。
- 但此時項目方不能把這些Ether 轉給我們,因為我們不能保證能夠還得上。因此Vee 將這些借款鎖在一個代理(Proxy)合約中,由代理合約代為交易。
- 假設我們看好LINK 代幣,想用槓桿放大獲利。假設此時市場價1 LINK = 0.01 Ether。
- 為了(從我們對市場的觀點)中獲益,我們現在將1.5 個Ether 換為150 個LINK (第一筆交換),並讓Vee 幫我創建一個限價單,當1 LINK = 0.02 Ether 時將所有的LINK 換回Ether (第二筆交換)。這些功能正是Vee 相對Compound 的創新。
- 假設我們是正確的,一段時間後LINK 的價格上漲。此時我們在Vee 中的限價單執行,150 個LINK 換到3 個Ether。訂單完成,我們獲利1.5Ether。
- 假設我們是錯誤的,一段時間後LINK 的價格下跌,Vee 會持續監控流動性情況,當可能產生資不抵債時,Vee 會主動將限價單取消並將所有的LINK 賣出以清算我們的所有賬戶。
這個邏輯的正常執行基於以下條件:Vee 在槓桿交易開始時,必須立刻檢查第一筆交換的資金是否具有相同價值(在去除正常的滑點等因素之後),並持續監控用戶的流動性,在流動性不足時及時清算。在上述第4 步中,1.5 個Ether 和150 個LINK 的價值是基本等價的。否則,假設150 個LINK 的價值只有0.5Ether,相當於Vee 用1.5 個Ether 換到了價值只有0.5 個Ether 的代幣,會導致項目方嚴重虧損。
然而,我們在仔細分析了Vee 項目的代碼之後,發現整個發起槓桿交易的borrowAndCall 調用並沒有對第一筆交換的價值進行判斷。同樣以上面的例子為例,在調用borrowAndCall 時,由於交易對不平衡等因素,1.5 個Ether 只換回了50 個LINK (價值只有0.5 個Ether)。此時Vee 項目方應該檢查這筆交換的價值,判定該調用失敗:因為兌換的兩種代幣價值不對等,此時用戶已經處於虧空狀態,項目方已經受損。遺憾的是,由於缺乏檢查,此次調用成功執行,此時項目方已經處於無法挽回的虧損狀態。
按道理來說,只要交易量足夠大,不平衡的交易對會由於套利等因素逐漸平衡。對於正常的Pangolin 交易池來說也是如此,小額交易只會產生少量滑點,因此代理合約使用用戶的借出額進行交易,確實是將資金池內的某種代幣換成了價值相等的另一種代幣。
然而我們發現,作為Vee 官方支持的LINK 代幣,在其依賴的Pangolin 交易池中竟然沒有交易對! Pangolin 項目的代碼和Uniswap 基本一致,相同的代幣對只能創建一個資金池,而攻擊者利用的ETH-LINK 資金池,正是由攻擊者自行創建的。當然,即使攻擊者沒有創建惡意的不平衡交易對,也可以用Flash Loan 的方式使交易對不平衡。
綜上,可以推斷出下面的攻擊流程:
- 創造一個不平衡的交易對並在Pangolin 上註冊。舉例來說,創造一個用1 個LINK 就可以換出100 個Ether 的交易對P。
- 調用borrowAndCall,加上槓桿,借出大量的Ether。指定的訂單內容是在交易對P 上將Ether 換成LINK。
- 訂單執行,交易對P 中多了很多藉出來的Ether,還給Vee 的代理合約的只是非常少量的LINK。至此,攻擊者成功地「套」出了Vee 的Ether。
- 在交易對P 上進行交易,用少量USDT 換出前一步中套出的Ether,獲得大量獲利。
總結來說,對於槓桿交易的實現,項目方應當在槓桿交易的開始就檢查第一筆交換的前後價值,如果嚴重不對等,此時用戶的流動性已經出現虧空,則應該直接使調用失敗,避免進一步的損失。
攻擊交易分析
以下,我們將以攻擊交易
0x4fb222908bd87cda0336776a6d78d35ef77b0a4bad866c4530b9f0d2616af005
為例介紹攻擊的主要流程。如圖所示(圖中省略了一些影響不大的合約調用)。
- 圖中步驟1,攻擊合約一先為攻擊合約二往VeeFinance 的cToken 地址(合約名稱為CErc20Immutable)存入了約0.96WETH,從而使得合約二可以進行抵押借貸(可以藉出約0.52WETH)及槓桿交易。
- 圖中步驟2,合約一通過創建合約二的方式繞過了isContract() 對msg.sender 的檢查,並在constructor() 函數中進行攻擊調用。
- 圖中步驟3~5,合約二調用CErc20Immutable 的borrowAndCall() 函數,正如前面代碼分析的,該函數在設置leverage 倍數之後將進行槓桿交易,通過調用VeeProxyController 去Pangolin (類似於UniswapV2)的池子中進行交易。注意,該池子是攻擊者在攻擊之前創建的交易對,所以池子的滑點受到攻擊者的控制,導致VeeFinance 的合約用加了槓桿後的約1.55 個WETH 只換回了約0.27 個LINK,造成了大量虧損。
- 圖中步驟6~8,合約二還上借貸,取出圖中1 的抵押物並transfer 給合約一。前面代碼分析中指出,雖然攻擊者通過抵押槓桿進行了遠超抵押額的虧本交易,但是系統記錄的用戶借款卻仍然只有加了槓桿之前的借貸量。因此合約二僅需還上約0.52 的WETH,就能取出圖中1 存入的0.96WETH。
- 圖中步驟9,合約一以少量(約0.27 個) LINK 換出了約1.55 個WETH,從而獲得了步驟6~8 中Vee 合約的虧損,從而攻擊獲利。
在我們分析的這筆交易中,攻擊者這樣的操作共進行了5 次。而通過反複利用漏洞連續攻擊,攻擊者的最終獲利頗為可觀。
總結
針對DeFi 項目的攻擊已出現多次,不斷地給項目方和投資者帶來嚴重的損失。值得注意的是,本次攻擊的原理與2020 年初發生的bZx 安全事件第一次攻擊的原理非常相似。而相似的攻擊不斷重複表明,過往的經驗和教訓尚未得到應有的重視,DeFi 安全還有很長的路要走。