今日最轟轟烈烈的新聞,非當紅炸子雞-Defi 中,急速竄起卻又急速殞落的超興星-小蕃薯 YAM 莫屬。8 月 13 日,備受矚目的 DeFi 項目 YAM Finance 宣布啟動流動性挖礦,一天時間鎖倉資產價值就超過了 6 億美元;然而不到幾小時,僅一行代碼就將數億美元全數蒸發。
(前情提要:挖蕃薯風波|Yam Finance 爆智能合約漏洞!Defi 項目未經審計釀隱憂?)
36 小時內,眼看他起高樓;幾分鐘內,眼看他樓塌了
台灣時間 8月13日上午 3 時整,備受矚目的 DeFi 項目 YAM Finance 宣布啟動流動性挖礦,僅僅一天時間鎖倉資產價值就超過了 6 億美元,其鎖定資產增量和增速都達到了近乎癲狂狀態。且照此發展,一些早期給池子注入流動性的羊毛黨年利率甚至可逼近 200倍,其瘋狂程度可見一斑。
不過,正當大家都陷入挖礦狂歡的時候,意外發生了。
台灣時間 8 月 13 日凌晨,YAM Finance 發現其智能合約的彈性供應機制(rebase)存在漏洞,導致合約第二次 rebase 觸發時會鑄造大量額外代幣,這意味著未來社區將無法獲得足夠的代幣來執行任何治理操作,YAM 將成為一個失控的機器,最終將徹底失去社群用戶的信任。
該如何拯救我們的 YAM 小蕃薯呢?
在發現漏洞後,YAM 團隊發起了「拯救行動」,稱他們需要 16 萬委託投票才能提交治理提案,於是向社群發起呼籲投票。很快,這場轟轟烈烈的社區投票行動就完成了。
然而,就在大家以為只是虛驚一場的時候,台灣時間 8 月 13 日下午 16 時 1 分,YAM 創辦人 Brock Elmore 卻發推特稱,對不起大家,我失敗了。這究竟是怎麼回事呢?
PeckShield 安全人員介入分析後,迅速定位到問題的本質在於:彈性供應機制(rebase)存在一個代碼公式的錯誤,致使第二次 rebase 觸發時系統會自動增發10 ^ 18個新代幣,如果行情一直保持高位的話,那麼以後的每次 rebase 觸發時都會進行指數級的增發,這將使小紅薯 YAM 的數量變成一個可怕的天文級數字。這意味著,無論後期社群怎樣委託投票,都無法獲得足夠的投票量對系統進行控制,整個系統將陷入失控無主狀態。
本來 YAM 官方號召廣大 YAM 持有人通過代理投票的方式,一起完成此次投票「拯救行動」,以修復這個存在的漏洞。然而,我們進一步分析發現,當 YAM 官方開始發出呼籲的時候,這次拯救行動其實就已經註定失敗了。
原因有二:
1)時間來不及: YAM 官方或許忽略了一點,在其提案投票拯救行動準備工作完成後,也需要至少 12.5 個小時才能被執行生效,而按照現在的時間節奏,當其執行生效時,第二次 rebase 早已觸發。
2)新部署治理合約無法被有效執行:由於第二次 rebase 觸發,因此官方原先預期要執行的新治理合約到了執行時間後,卻發現由於投票總量遠遠無法達到合約約定的總量的 4 %,故而無法被有效執行。
究竟是為何呢?接下來上技術乾貨:
技術概要
首先介紹下 YAM 智能合約的彈性供應機制(rebase):
1)系統會根據市場價格浮動來動態調整代幣的供應量,當市價上漲時則按比例增發代幣,以降低單位代幣的價值,直至降至 1 美元。
2)每天分別執行兩次 rebase,每次 rebase 會改變代幣供應量,根據市場現價增發或銷毀一定量的代幣。
再說一個實施提案的關鍵因素:持有者進行委託投票,投票數超過總量的 1%,則提案才可以進行執行排列,且按合約約定執行排列時長需要等待 12.5 小時,而提案執行時,則投票需要超過總量的 4%。如此新治理合約才能執行生效,項目才能繼續正常運轉。
有了以上幾個技術要點的鋪墊,我們再來看一下,YAM 官方的跟進時間表,就能明白此次拯救行動為什麼注定會失敗。
如下圖時間線所示:
①之後的綠色區域是投票和提案拯救行動可以成功的「黃金急救期」,需要整個拯救行動準備工作在第一次 rebase 觸發之前半小時內完成。(即藍色虛線應提前到綠色區域內)。
②是第一次 rebase 觸發的時間,由於合約的 bug 導致 totalsupply 資產發生異常暴漲,官方發現 BUG 存在並進行了披露。
③是官方宣布提議部署新治理合約的時間,在此之後社區開始啟動投票。
④是投票目標初步完成,新治理合約進入執行排列的時間,自此等待執行12.5小時合約正式執行。
⑤是第二次 rebase 的觸發時間。
⑦是其新治理合約投票通過後正式執行的時間。
⑥ 在第二次 rebase 觸發後的第 31 分鐘時,或許是項目方發現了已經無力回天了,提案取消成功,項目方正式宣布 YAM 失敗。
這意味著,YAM 官方應在第一次 rebase(台灣時間 8月 13 日凌晨 04:08)之前就應該發現這個漏洞,並且留有足夠的時間完成新治理合約部署和投票。
可是事與願違,官方發現漏洞並披露呼籲投票的時間還是太晚了,錯過了唯一能夠成功的黃金急救期。而更糟糕的是,按照官方的時間節奏,當新治理合約到了 ⑦ 執行的時間後,投票數要超過總量的 4% 才行,而此刻的總量已經擴大了 10^18* 10^18,此前累積的投票數已然杯水車薪,根本無濟於事。
所以,這次拯救行動一開始就注定了會失敗。
詳細過程分析
首先我們看下當第一次 rebase 發生了什麼:
如上面鏈上訊息(https://oko.palkeo.com/0x7b9017ec92b0200455e5269380195fbecfbf91c8acda30985cc1dc413d215076/)所示,當第一次 rebase 之後,totalSupply 從 3,500,000* 10^18 暴漲到一個極大值。
我們進一步分析代碼,看下在代碼中發生了什麼:首先從鏈上信息我們能看到 rebase 操作調用的是 YAMRebaser 合約的 YAMRebaser::rebase() 函數(我們先跳過這個函數稍後再講) ,我們最終發現它通過調用 YAM 合約(0xa923af6d05993495257a872ec69dbbf01501eb0e)的 rebase() 函數重新計算totalSupply(代碼邏輯如下圖2中所示),在第 340 行的 totalSupply 賦值操作可以看到,這一行代碼有個明顯的錯誤——沒有除 BASE,從而導致 totalSupply 的值暴增了 10^18 倍。
YAM 官方在第一次 rebase 以後發現了這個問題,於是披露 rebase bug 事件啟動了投票拯救行動。
而在 12 小時之後,YAM 又觸發了第二次 rebase(https://oko.palkeo.com/0x32735e9e9aac51739b5725a225be6c7a3851f422be986d0f4f4bc0ec475ee286/),這個數據又是以基於錯誤的 totalSupply 來計算的,從而導致 initSupply 的數值同樣出現了異常。
我們繼續分析造成 initSupply 異常的成因,關鍵在上面提到過 YAMRebaser::rebase() 函數,這個函數實現的主要邏輯:先基於 yam.totalSupply() 計算出本次 rebase 需要增發的 YAM 數額 mintAmount,在 afterRebase() 函數經過數層調用後進入 YAM 的 _mint() 函數,基於異常的 mintAmount 給 initSupply 進行賦值。
由於在第一次 rebase 中,totalySupply 已經變成一個極大值,所以基於此異常值的後續一列操作(如圖4中紅色箭頭所示)最終導致 initSupply 也計算錯誤,變成了一個天文級的數值。
當第一次 rebase 出現異常時,項目方已經發現問題並決定提出一個修復系統的提案(proposal),希望通過投票的方式將此提案排入執行隊列並且執行。當此提案收到足夠多的投票,治理合約(Governor)允許任何人通過調用 GovernorAlpha::queue() 函數將此題案排入執行隊列。
但由於此治理合約代碼邏輯的實現,導致無論是在第二次 rebase之 前或是之後進行修復,都無法正確執行這個拯救行動。
為什麼說項目方准備工作完成的太晚了?
我們看下圖中的 GovernorAlpha::queue() 代碼,我們注意到了在調用 _queueOrRevert 函數之前的第 224 行中設置變量 eta = current timestamp + timelock.delay(12.5小時),這就使得生效時間必然在加入隊列的 12.5 小時以後,而第二個 rebase 時間是與第一次間隔 12 小時,這就意味著要執行成功需要將拯救行動提前到第一次 rebase 之前至少半小時以上,否則將永遠無法執行。
又為什麼說已經做出的拯救行動,根本無濟於事呢?
當觸發合約 GovernorAlpha::execute() 時首先會先執行 state 函數來獲得當前提案狀態。
在下面的 state() 函數第 330 行,如果 proposal.forVotes <= againstVotes() ,提案狀態被設置為失敗。
從代碼中能看出來,項目方在設計系統時,投票數被設計為必須大於 initSupply 總量的 4%,此提案才能是合法的狀態,如下圖所示。然而,當第二次執行 rebase 以後, initSupply 已經被搞成一個極大值。這就導致了,投票票數(forVotes)永遠不可能>= quorumVotes(),從而總是返回 Defeated。
除了提案狀態異常的問題之外,如圖9、圖10所示,當第二次 rebase 發生以後,由於 GovernorAlpha :: propose() 檢查投票數必然小於 proposalThreshold(1%的initSupply),因此新的提案也再也無法被提出,更遑論要投票執行了。
總結
此次 YAM 漏洞事件,最終造成治理合約中 75 萬枚 yCRV 被永久鎖定,而且短時間內的急速暴跌和無力回天的局面,不知道有多少人被埋在了價格高點,其瘋狂程度成瞭如今 DeFi 流動性挖礦的最真實寫照,其殘酷魔幻程度何嘗又不是?倘若項目方在部署合約之前但凡測試過一次 rebase 流程,必定能捕捉到漏洞的存在。足以見得,DeFi 項目做安全審計的重要性。
在區塊鏈世界裡,務必要對每一行合約代碼保持敬畏,因為任何細微的疏漏都可能造成無法挽回的局面。畢竟,代碼是人寫的,漏洞也很難被徹底避免,因此需要項目方在合約部署上線前就做好充分的測試和第三方安全審計工作,這會幫助其更早發現並排查合約代碼潛在的安全漏洞,不至於等到,漏洞發生後,亡羊補牢,為時已晚。
📍相關報導📍
「對不起,我失敗了」Yam Finance沈痛宣告項目死亡幣暴跌99%,待2.0再出發
挖蕃薯崩潰風波|技術解析:YAM 的「一行程式碼」如何蒸發數億美元?
DeFi捷報》超越BCH!Chainlink成市值排名第五;Yam效應驅動,Maker大漲近 40%
讓動區 Telegram 新聞頻道再次強大!!立即加入獲得第一手區塊鏈、加密貨幣新聞報導。
LINE 與 Messenger 不定期為大家服務