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(soVELO 合約),並設置該 market 的一些關鍵配置,比如設置利率模型,設置價格預言機,設置抵押因子等。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 代幣增多,但是由於沒有新的 soVELO 代幣的鑄造,因此 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 」(幾乎是所有攻擊者此前轉入或者抵押進 soVELO 合約的 VELO 代幣數量),此時需要根據公式「 redeemTokens = redeemAmountIn / exchangeRate 」來計算贖回所需要銷燬的 soVELO 代幣的數量。
從「 exchangeRateStoredInternal 」函數可以看出,由於此時 _totalSupply 是 2 不是 0,因此需要計算 exchangeRate 的值,通過公式「 exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply 」計算出,目前的 exchangeRate 爲「 17,735,851,964,756,377,265,143,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 -> 0x7e97b74252b6df53caf386fb4c54d4fb59cb6928 -> 0xc521bde5e53f537ff208970152b75a003093c2b4 -> 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) 共同撰寫。