Avalanche 生態借貸項目 Vee Finance 因「槓桿借貸」功能不完善而遭價格操縱攻擊,損失或超過 3500 萬美元。本次攻擊原理與 2020 年初發生的 bZx 安全事件第一次攻擊原理非常相似。

原文標題:《似曾相識燕歸來:Vee Finance 安全事件分析》
撰文:BlockSec

北京時間 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 函數中實現:

BlockSec:技術分析 Vee Finance 合約漏洞及被攻擊流程

函數實現首先調用 borrowLeverageInternal 函數進行槓桿借貸,槓桿比率由參數 leverage 指定。然後調用 callOrderProxyInternal 執行用戶指定的交易(Order)。

下面首先來看 borrowLeverageInternal (內部會調用邏輯實現 borrowFresh)的實現。大部分代碼與 Compound 相同,只有處理 leverage 的時候存在不同:

BlockSec:技術分析 Vee Finance 合約漏洞及被攻擊流程

當用戶指定的槓桿爲 0 時,執行正常的借貸邏輯,把資金直接轉給借貸人;當存在槓桿時,將槓桿記錄在狀態變量中,然後調用 doDeposit 函數將資金轉移到某個固定的位置(防止用戶直接取走)。

BlockSec:技術分析 Vee Finance 合約漏洞及被攻擊流程

需要注意的是,borrowLeverageInternal 的實現除了上述對 leverage 的處理之外,和原版 Compound 並沒有任何不同。也就是說,對於這一筆槓桿借貸,系統只會記錄單倍槓桿的借貸額。也就是說,雖然用戶用 100 美元抵押借出了 240 美元,但系統記錄的用戶借款仍然只有 80 美元。

doDeposit 函數的實現很簡單,將資金轉入 orderProxy 合約中。

BlockSec:技術分析 Vee Finance 合約漏洞及被攻擊流程

callOrderProxyInternal 函數的實現也很簡單,就是將用戶指定的 order 發給 orderProxy 進行執行。

總結一下:用戶可以進行抵押借貸。當使用抵押借貸時,資金不會轉給用戶,而是轉給 Vee 控制的 orderProxy 合約,後者會執行一個用戶指定的訂單。

漏洞成因分析

我們回顧一下一個正常的槓桿交易訂單是怎樣操作的:

  1. 假設我們擁有 1 個 Ether,想用槓桿來增加獲利,Vee 正好提供了槓桿交易的功能。

  2. 我們抵押了 1 個 Ether,根據超額抵押原則,能借出 0.5 個 Ether;再加上三倍槓桿,總共可以借出 1.5 個 Ether。

  3. 但此時項目方不能把這些 Ether 轉給我們,因爲我們不能保證能夠還得上。因此 Vee 將這些借款鎖在一個代理(Proxy)合約中,由代理合約代爲交易。

  4. 假設我們看好 LINK 代幣,想用槓桿放大獲利。假設此時市場價 1 LINK = 0.01 Ether。

  5. 爲了(從我們對市場的觀點)中獲益,我們現在將 1.5 個 Ether 換爲 150 個 LINK (第一筆交換),並讓 Vee 幫我創建一個限價單,當 1 LINK = 0.02 Ether 時將所有的 LINK 換回 Ether (第二筆交換)。這些功能正是 Vee 相對 Compound 的創新。

  6. 假設我們是正確的,一段時間後 LINK 的價格上漲。此時我們在 Vee 中的限價單執行,150 個 LINK 換到 3 個 Ether。訂單完成,我們獲利 1.5Ether。

  7. 假設我們是錯誤的,一段時間後 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 的方式使交易對不平衡。

綜上,可以推斷出下面的攻擊流程:

  1. 創造一個不平衡的交易對並在 Pangolin 上註冊。舉例來說,創造一個用 1 個 LINK 就可以換出 100 個 Ether 的交易對 P。

  2. 調用 borrowAndCall,加上槓杆,借出大量的 Ether。指定的訂單內容是在交易對 P 上將 Ether 換成 LINK。

  3. 訂單執行,交易對 P 中多了很多借出來的 Ether,還給 Vee 的代理合約的只是非常少量的 LINK。至此,攻擊者成功地「套」出了 Vee 的 Ether。

  4. 在交易對 P 上進行交易,用少量 USDT 換出前一步中套出的 Ether,獲得大量獲利。

總結來說,對於槓桿交易的實現,項目方應當在槓桿交易的開始就檢查第一筆交換的前後價值,如果嚴重不對等,此時用戶的流動性已經出現虧空,則應該直接使調用失敗,避免進一步的損失。

攻擊交易分析

以下,我們將以攻擊交易
0x4fb222908bd87cda0336776a6d78d35ef77b0a4bad866c4530b9f0d2616af005
爲例介紹攻擊的主要流程。如圖所示(圖中省略了一些影響不大的合約調用)。

BlockSec:技術分析 Vee Finance 合約漏洞及被攻擊流程

  • 圖中步驟 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 安全還有很長的路要走。