2024 年5 月15 日,Sonne Finance 在Optimism 鏈上遭受攻擊,損失高達2 千萬美元。
攻擊發生後,X 上@tonyke_bot 用戶發推文表示,其用約100 美元保護了Sonne Finance 的代幣抵押池(也稱為market,類似於Compound 中的cToken)中剩餘的約650 萬美元。
(https://twitter.com/tonyke_bot/status/1790547461611860182)
Sonne Finance 專案方發現攻擊之後,迅速暫停了Optimism上的所有markets,並表示Base 上的markets 是安全的。
(https://twitter.com/SonneFinance/status/1790535383005966554)
攻擊簡述
Sonne Finance 是Optimism 上的一個fork 了Compound V2 的去中心化借貸協議,供個人、機構和協議訪問金融服務。 Sonne Finance 協議將用戶的token 資產聚合起來,形成了借貸流動性池,為用戶提供了一個類似銀行的借貸業務。與Compound 一樣,協議參與者可以將其持有的token 抵押到Sonne Finance 的借貸流動性池中,同時獲得憑證soToken(與cToken 一樣)。而soToken 是一種生息資產憑證,隨著區塊的推進會產生一定的收益,同時還會獲得SONNE token 激勵。而參與者憑藉著手裡的soToken 還能從Sonne 借貸資產池中藉出其他token,例如參與者可以抵押一定數量的USDC 獲得soUSDC 憑證,隨後藉貸出WETH用於經一步的流通。 Sonne Finance 協議中的抵押貸款借貸可以是多對多的資產關係,在抵押借貸的過程中,協議會自動計算參與者地址的健康度(Health Factor),當健康度低於1 時,該地址的抵押品將支持被清算,而清算者也能獲得一定的清算獎勵。
使用者存入的underlying token 與鑄造的soToken 的數量關係,主要與一個叫做exchangeRate 的變數有關,這個變數粗略可以用來表示每個soToken 價值多少underlying token。 exchangeRate 的計算公式如下:
在上述公式中,totalCash 是指soToken 持有的underlying token 的數量,totalBorrows 是指某market 中被借出去的underlying token 的數量,totalReserves 是指總儲備金數量(其中包含借款人支付的利息),totalSupply是指鑄造的soToken 的數量。
在贖回時,使用者可以指定想要贖回的underlying token 的數量redeemAmount,來計算需要銷毀掉的soToken的數量redeemTokens,計算方式大概為“ redeemTokens = redeemAmount / exchangeRat ”,注意這裡並沒有對精度損失做處理。
這次攻擊事件的本質是market (soToken) 被創建出來時,攻擊者進行了第一筆抵押鑄造的操作,以少量underlying token 鑄造了很少的soToken,導致soToken 的“ totalSupply ”數值太小。攻擊者繼而利用了Solidity 合約精度損失這個漏洞,再搭配直接往soToken 合約發送underlying token(不會鑄造soToken,也就意味著“ totalSupply ”不變,“ totalCash ”變大),而不是抵押+ 鑄造的方式存入underlying token。這樣的操作使得合約中「 totalCash 」 變數變大,但是「 totalSupply 」 保持不變,從而導致exchangeRate 變大。當最終攻擊者在贖回underlying token 時,需要銷毀的soToken 少於抵押時鑄造的soToken,攻擊者利用賺取的soToken 去其他的soToken(比如soWETH、soUSDC)中藉出underlying token WETH、USDC,最終獲利高達2000 萬美元。
攻擊中涉及的關鍵地址
攻擊準備交易:
https://optimistic.etherscan.io/tx/0x45c0ccfd3ca1b4a937feebcb0f5a166c409c9e403070808835d41da40732db96
攻擊獲利交易:
https://optimistic.etherscan.io/tx/0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0
攻擊EOA 相關地址:
0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb 0xae4a7cde7c99fb98b0d5fa414aa40f0300531f43
攻擊者(合約)相關地址:
0xa78aefd483ce3919c0ad55c8a2e5c97cbac1caf8 0x02fa2625825917e9b1f8346a465de1bbc150c5b9
underlying token(VELO Token V2):
0x9560e827af36c94d2ac33a39bce1fe78631088db
漏洞合約(soVELO,類似Compound 的cToken):
0xe3b81318b1b6776f0877c3770afddff97b9f5fe5
X 上@tonyke_bot 用戶救援交易:
https://optimistic.etherscan.io/tx/0x816f9e289d8b9dee9a94086c200c0470c6456603c967f82ab559a5931fd181c2
攻擊流程分析
前情提要
Sonne Finance 專案方最近通過了一項將VELO market 添加到Sonne Finance 的提案(https://twitter.com/SonneFinance/status/1786871066075206044),並透過多簽錢包安排了五筆在兩天之後執行的交易( https://optimistic.etherscan.io/tx/0x18ebeb958b50579ce76528ed812025949dfcff8c2673eb0c8bc78b12ba6377b7),這五筆交易是用來定義價格因子等。 VELO market 創建之後,用戶可以存入VELO 代幣,以鑄造soVELO 代幣,soVELO 代幣又可以用來借貸其他soToken。
攻擊準備
攻擊準備階段主要是攻擊者在提案兩天鎖定時間結束後,根據Sonne Finance 項目方提案中的信息,創建VELO market(soVELO 合約),設置關鍵的配置,並通過抵押VELO 代幣進soVELO 合約來鑄造soVELO 代幣,同時也將自己持有的VELO 代幣以直接發送給soVELO 合約的方式,來增大exchangeRate,為後續攻擊獲利做準備。
具體步驟如下:
-
攻擊者在兩天鎖定時間結束後,首先將提案中安排的前四筆交易的操作打包到一筆交易中(交易0x45c0cc),用來創建VELO market(soVELO 合約),並設定好關鍵的配置。 VELO market 初始化時,exchangeRate 被設定為「 200,000,000,000,000,000,000,000,000 」。
-
攻擊者調用soVELO 合約的「 mint 」函數來存入VELO 代幣,並鑄造soVELO 代幣,攻擊者指定「 mintAmount 」為「 400,000,001 」(VELO 代幣的數量)。從函數「 exchangeRateStoredInternal 」可以看出,由於此時soVELO 代幣的「 _totalSuppl 」是0,因此exchangeRate 即為第1 步驟設定的值。根據公式「 mintTokens = actualMintAmount / exchangeRate 」,此時計算出的應該鑄造的soVELO 代幣的數量為2。簡而言之,這一步驟攻擊者向soVELO 合約中存入數值為「 400,000,001 」 的VELO 代幣,攻擊者獲得數值為2 的soVELO 代幣。
soVELO.mint:
-
攻擊者以直接給soVELO 合約發送VELO 代幣的方式,給soVELO 合約發送了數值為「 2,552,964,259,704,265,837,526 」的VELO 代幣,此時soVELO 合約持有的VELO 代幣增加的VELO鑄造,因此totalSupply 保持不變,也就意味著此時根據exchangeRate 計算公式計算出的exchangeRate 會變大。
-
攻擊者將持有的soVELO 代幣轉移多次,最終轉移給了另一個攻擊EOA 0xae4a。
攻擊獲利
攻擊獲利階段主要是攻擊者執行提案的第五筆交易,並透過閃電貸借出VELO 代幣直接發送給soVELO 合約,以進一步增大exchangeRate。然後攻擊者利用自己手上的數值是2 的soVELO 代幣,去其他的soToken(比如soWETH,soUSDC 等)合約中藉出了WETH、USDC 等underlying token,這些部分成為了攻擊者獲利。緊接著攻擊者去soVELO 合約中贖回自己的underlying token,由於exchangeRate 變大,以及計算贖回需要銷毀的soVELO 代幣時的精度損失問題,最終使得攻擊者僅使用數值為1 的soVELO 代幣就贖回了先前存入的幾乎全部的VELO 代幣,可以理解為攻擊者利用多得的數值為1 的soVELO 代幣,透過從其他soToken 借貸賺取了WETH、USDC 等underlying token。攻擊者使用同樣的手法多次重複攻擊,最後獲利巨大。
具體步驟如下:
-
攻擊者執行題案中的第五筆交易,設定提案中規定的借貸因子。
-
攻擊者從VolatileV2 AMM – USDC/VELO 池子中閃電貸出數值為「 35,469,150,965,253,049,864,450,449 」的VELO 代幣,這會觸發攻擊者的hook 函數。在hook 函數中,攻擊者繼續執行攻擊操作。
-
攻擊者將自己持有的VELO 代幣發送給soVELO 合約,以進一步增加exchangeRate。目前soVELO 合約中一共有數值為「 35,471,703,929,512,754,530,287,976 」的VELO 代幣(攻擊者三次轉入的VELO 代幣和)。
-
攻擊者創建新的合約 0xa16388a6210545b27f669d5189648c1722300b8b,在構造函數中,將持有的2 個soVELO 代幣轉給新創建的合約0xa163(以下稱為攻擊者0xa163)。
-
攻擊者0xa163 以持有的soVELO 代幣,從soWETH 借出數值為「 265,842,857,910,985,546,929 」的WETH。
-
攻擊者0xa163 呼叫soVELO 的「 redeemUnderlying 」函數,指定贖回VELO 代幣的數值為「35,471,603,929,512,754,530,287,976 」(幾乎是所有攻擊者先前轉入代幣數量redeemTokens = redeemAmountIn / exchangeRate 」來計算贖回所需銷毀的soVELO 代幣的數量。
從「 exchangeRateStoredInternal 」函數可以看出,由於此時_totalSupply 是2 不是0,因此需要計算exchangeRate 的值,透過公式「 exchangeRate = (totalCash + totalBorrows – totalReserves) / totalSupply 」計算出,目前的exchangeRate 為「 988,000,000,000,000,000,000 ”,這個值遠大於設定的初始exchangeRate “ 200,000,000,000,000,000,000,000,00 ”。
根據新的exchangeRate 計算出的「 redeemTokens 」的值為「 1.99 」,由於Solidity 向下取整的特性,「 redeemTokens 」的值最終為1。也就意味著攻擊者0xa163 使用數值為1 的soVELO 代幣,贖回了先前存入的幾乎所有的VELO 代幣。同時攻擊者0xa163 也賺取了從soWETH 借出的數值為「 265,842,857,910,985,546,929 」的WETH。
soVELO.redeemUnderlying:
soVELO.exchangeRateStoredInternal:
-
攻擊者0xa163 將藉到的WETH 和贖回的VELO 代幣全部轉給了上層攻擊者,然後自毀。
-
攻擊者呼叫soWETH 的「 liquidateBorrow 」函數,用來清算前面新建立的合約0xa163 借貸的部分資產,目的是拿回鎖定住的數值為1 的soVELO 代幣。目前攻擊者只持有數值1 的soVELO 代幣。
-
攻擊者調用soVELO 的「 mint 」函數,再一次抵押鑄造soVELO 代幣,目的是湊夠數值為2 的soVELO 代幣,然後再次執行上述第3-8 步,獲利其他的undeylying token。
-
攻擊者執行數次第9 步的操作,還掉閃電貸,獲利離場。
$100 如何撬動$650 萬
攻擊發生後,X 上@tonyke_bot 用戶在交易0x0a284cd 中,透過抵押1144 個VELO 代幣到soVELO 合約中,鑄造了0.00000011 個soVELO。這樣操作之所以能夠阻止攻擊者進一步攻擊,是因為這筆交易改變了soVELO 中totalSupply 的大小和持有的VELO 代幣的數量totalCash,而totalSupply 增長對於計算exchangeRate 產生的影響大於totalCash 增長產生的影響,因此exchangeRate 變小,從而導致攻擊者進行攻擊時,無法再利用精確度損失賺取soVELO,導致攻擊無法再進行。
資金追蹤
攻擊者攫取非法收益後不久便將資金進行了轉移,大部分資金轉移到了以下4 個地址當中,有的是為了換個地址繼續攻擊,有的是為了洗錢:
-
0x4ab93fc50b82d4dc457db85888dfdae28d29b98d
攻擊者將198 WETH 轉入了該地址,然後該地址採用了相同的攻擊手法,在下列交易中獲得非法收益:
攻擊結束後,該地址將上述非法所得轉給了0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb。
-
0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb
攻擊者將724277 USDC、2353 VELO 轉入了該地址,並將USDC 兌換了Ether。隨後立即將部分資金轉入了Stargate 跨鏈橋,剩下大部分非法資金殘留在該地址中:
-
0xbd18100a168321701955e348f03d0df4f517c13b
攻擊者將33 WETH 轉入了該地址,並採用peel chain 的方式嘗試洗錢,洗錢鏈路如下:
0xbd18100a168321701955e348f03d0df4f517c13b -> 0x7e97b74252b6df53caf386fb4c54d459cb6928 -b6df53caf386fb4c54d459cb6928 ->>a505321 2b4 -> 0x9f09ec563222fe52712dc413d0b7b66cb5c7c795。
-
0x4fac0651bcc837bf889f6a7d79c1908419fe1770
攻擊者將563 WETH 轉入了該位址,隨後轉給了 0x1915F77A116dcE7E9b8F4C4E43CDF81e2aCf9C68,目前沒有進一步行為。
攻擊者這次洗錢的手段相對來說較為專業,手法呈現多元趨勢。因此對於我們Web3 參與者來說,在安全方面要持續不斷地提高我們的反洗錢能力,透過KYT、AML 等相關區塊鏈交易安全產品來提高Defi 專案的安全性。
安全建議
-
精度損失需重視。精度損失導致的安全問題層出不窮,尤其是在Defi專案中,精度損失往往導致嚴重的資金損失。建議專案方和安全審計人員仔細審查專案中存在精確度損失的程式碼,並做好測試,盡量規避漏洞。
-
建議類似於Compound 中cToken 這種market 的創建和首次抵押鑄造操作由特權使用者來執行,避免被攻擊者操作,從而操作匯率。
-
當合約中存在關鍵變數依賴「 this.balance 」或「 token.balanceOf() 」的值時,需要慎重考慮該關鍵變數改變的條件,例如是否允許直接透過給予合約轉原生幣或代幣的方式改變該變數的值,還是只能透過呼叫某特定函數才能改變該變數的值。
本文由ZAN Team 的Cara(X 帳號@Cara6289)和XiG(X 帳號 @SHXiGi) 共同撰寫。