在昨天(2021年9月22日)Vee Finance專案發生攻擊事件之後,我們第一時間對該事件開展分析併發布了初步的分析報告(似曾相識燕歸來:Vee Finance 安全事件分析)。但在分析過程中,依然存在一些懸而未決的疑點:
在攻擊交易中createOrderERC20ToERC20函式呼叫,有一個cToken行為比較奇怪,和這個地址相關的交易只有幾十條;後續查明這個cToken是由攻擊者控制的賬戶建立的。
在重新Review Vee專案的程式碼中,我們發現前一篇文章中發現整個發起槓桿交易的borrowAndCall呼叫並沒有對第一筆交換的價值進行判斷這個分析是不確切的。下文會闡述,在整個呼叫過程中存在對交換前後的價值進行判斷的程式碼邏輯。
在分析攻擊交易的Trace中,我們發現了一個奇怪的BTC代幣地址,該地址和前一篇文章所述的攻擊過程並無關聯。
在我們的分析報告發布之後,專案方也公佈了自己的官方分析報告(The Main Cause of Vee Finance Attack: https://veefi.medium.com/the-main-cause-of-vee-finance-attack-7a8475085ec5),然而該報告依然無法解釋上述疑問。鑑於此,我們對Vee專案和此次攻擊事件進行了更為細緻的分析和覆盤。分析結果表明,導致此次攻擊的根本原因是校驗機制的缺陷,而非如官方分析報告宣稱的,諸如單一價格預言機等因素帶來的影響。
0x1. 深入分析Vee專案程式碼
在後續對Vee專案方的程式碼進行深入分析的過程中,我們發現上述的檢驗過程其實是存在的,只是在攻擊者巧妙利用下檢查被繞過。下面我們來分析如何檢查和攻擊者如何繞過的過程:
首先在createOrderERC20ToERC20函式中,紅色箭頭標註的getAmountOutMin呼叫會對交換前後的價值進行檢查。當然,我們首先注意到在整個函式呼叫中,並沒有對cToken的真實性做檢查。也就是說,攻擊者可以自己建立一個cToken並呼叫createOrderERC20ToERC20函式建立一個訂單。這為攻擊者的攻擊埋下了伏筆。
在getAmountOutMin函式中,對第一次交換前後的價值是這樣做判斷的:
首先獲得傳入和傳出的cToken(ctokenA和ctokenB),從PriceOracle中呼叫getUnderlyingPrice獲得其Underlying代幣的價格。
計算呼叫calcSwapAmount,扣除交易費用,計算真正的swapAmountA = amountA * leverage * (1 - serviceFee)。
計算由Oracle返回的應該轉出的tokenA的估計量,即amountFromOracle = (priceA * swapAmountA) / priceB。
呼叫getAmountOut,返回從Pangolin交易所真正返回的轉出tokenA的數量amountOut。
對比Oracle返回的tokenA轉出估計量amountFromOracle和具體數量amountOut。如果amountFromOracle * 0.95 > amountOut,代表真正交易獲得的tokenB過少,則需要拒絕這筆交易。
第二次Review整個邏輯實現,我們注意到這裡有幾次呼叫cToken的underlying()函式的過程:
第一次在createOrderERC20ToERC20函式中,獲得了tokenA、tokenB,後續函式中幾乎所有需要用到Underlying的地方傳入的都是這兩個Token。
第二次在createOrderERC20ToERC20的getAmountOutMin呼叫中。如上文所述,這個呼叫的主要目的是檢驗此次Swap前後的價值是否一致,專案方本身是否受損。
那麼在getAmountOutMin中是怎麼檢驗的呢?我們重述一下這個過程:
重新呼叫underlying()函式獲得tokenA和tokenB。
從PriceOracle獲得ctokenA和ctokenB對應的Underlying價格。在這個過程中會再次呼叫underlying()獲得cToken對應的Underlying。
用第一步獲得的tokenA和tokenB,去Pangolin查詢能換得的tokenB數量,並與PriceOracle的數量進行比較。
一般來說這個過程是沒有問題的,這是由於正常的cToken合約,其Underlying是固定的。但是在整個過程中沒有對cToken是否真實進行驗證,這導致攻擊者可以傳入自己設定的cToken合約。
而攻擊者又是如何巧妙運用這個不一致性的呢?
在createOrderERC20ToERC20呼叫開頭,讓underlying()函式返回LINK代幣。因此後續真正執行的交易是WETH兌換LINK。
在getAmountOutMin函式中,讓underlying()函式返回BTC代幣,使這一步兌換價值校驗能夠透過。
更為巧妙的是,由於swapERC20ToERC20的第四個引數(如下圖所示)依賴getAmountOutMin函式返回的結果,因此攻擊者選取了BTC這個價值較高的代幣,使得合約對交換獲得的代幣數量的下限要求很低。
校驗完成後,真正執行的交易是在攻擊者建立的不平衡交易對中,將WETH兌換為LINK的交易,成功用少量的LINK套出了Vee合約的WETH。
透過巧妙地設計了underlying()函式,攻擊者成功地“狸貓換太子”,將(Vee合約認為的)BTC替換成了LINK。再根據之前所述的過程將Vee合約中鎖倉的流動性套出,完成了此次攻擊。
當然,專案方還是做了許多檢查。在createOrderERC20ToERC20中,合約呼叫了一個檢查函式進行檢查,其中對轉出Token做了檢查:
也就是說,每個槓桿交易的第一步交易,轉入的Token(也就是代理合約使用槓桿借入的資金換得的Token)必須是在專案方自己控制的白名單內的。
總結來說,專案方的兩個疏忽導致了此次攻擊:
沒有對使用者建立訂單時傳入的cToken進行驗證。任何人都可以建立一個cToken,然後建立這個cToken對應的訂單。
沒有在Pangolin建立專案支援Token的交易對,或者說沒有維持交易對的流動性。只有維持了交易對的流動性,槓桿交易的第一次交換才能換到等值的代幣。
0x2. 關於官方分析報告
在攻擊發生不久後,Vee專案官方釋出了分析攻擊原因的報告(https://veefi.medium.com/the-main-cause-of-vee-finance-attack-7a8475085ec5)。其中專案方認為導致攻擊的原因有以下幾個:
價格預言機只有一個價格來源,因此這個價格受到了市場波動的影響。
價格處理時沒有考慮不同Token的decimals可能不同。
交易對在交易時沒有設立白名單機制。
首先,Vee專案的價格預言機並沒有開源。但由於Vee是一個借鑑Compound的專案,而Compound的預言機是開源的,從原始碼中可以看出:
Compound的預言機在計算Underlying價格時是考慮了不同Token的decimals可能不同這種情況的。除非Vee專案對Compound預言機進行了大幅修改,否則預言機不太可能是導致此次攻擊的罪魁禍首。事實上,攻擊交易中預言機返回的報價如下圖所示:
注意這裡的16進位制值0x15e1549d1216fe9fc032e7c00000對應的十進位制值為443783124870000000000000000000000,正好是當時BTC的價格,可以作為預言機清白的旁證。
同樣在之前的分析中可以看出,Vee是對槓桿交易能夠換到什麼代幣做了嚴格的白名單檢查的。因此白名單檢查也不能為此次攻擊背鍋。
綜上所述,真正導致攻擊的問題在於校驗機制的缺陷:建立訂單的cTokenB(槓桿交易第一筆交易要兌換獲得的Token),其地址是使用者(透過order引數)可以完全控制的;而直到訂單執行的整個過程中,該地址都是被直接使用的,並未經過任何校驗。
0x3. 結語
本次攻擊手法隱蔽而巧妙,整個分析過程也是百轉千折。當然,在這一過程中我們也有很多收穫。安全之路上,“博學之,審問之,慎思之,明辨之,篤行之”,誠哉斯言!