2024 年 5 月 15 日,Sonne Finance 在 Optimism 鏈上遭受攻擊,損失高達 2 千萬美元。

作者:ZAN Team

封面: Sonne Finance

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 / exchangeRate注意這裡並沒有對精度損失做處理

這次攻擊事件的本質是 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,為後續攻擊獲利做準備。

具體步驟如下:

1. 攻擊者在兩天鎖定時間結束後,首先將提案中安排的前四筆交易的操作打包到一筆交易中(交易 0x45c0cc),用來創建 VELO market(soVELO 合約),並設定好關鍵的配置。 VELO market 初始化時,exchangeRate 被設定為 200,000,000,000,000,000,000,000,000

2. 攻擊者調用 soVELO 合約的 mint  函數來存入 VELO 代幣,並鑄造 soVELO 代幣,攻擊者指定 mintAmount  為 400,000,001(VELO 代幣的數量)。從函數 exchangeRateStoredInternal  可以看出,由於此時 soVELO 代幣的 _totalSupply  是 0,因此 exchangeRate 即為第 1 步驟設定的值。根據公式 mintTokens = actualMintAmount / exchangeRate,此時計算出的應該鑄造的 soVELO 代幣的數量為 2。簡而言之,這一步驟攻擊者向 soVELO 合約中存入數值為 400,000,001  的 VELO 代幣,攻擊者獲得數值為 2 的 soVELO 代幣。 soVELO.mint:

function mint(uint mintAmount) override external returns (uint) {
  mintInternal(mintAmount);
  return NO_ERROR;
}
 
function mintInternal(uint256 mintAmount) internal nonReentrant {
  accrueInterest();
  // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to
  mintFresh(msg.sender, mintAmount);
}
 
function mintFresh(address minter, uint256 mintAmount) internal {
  ......
 
  Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
 
  /////////////////////////
  // EFFECTS & INTERACTIONS
  // (No safe failures beyond this point)
 
  /*
   *  We call `doTransferIn` for the minter and the mintAmount.
   *  Note: The cToken must handle variations between ERC-20 and ETH underlying.
   *  `doTransferIn` reverts if anything goes wrong, since we can't be sure if
   *  side-effects occurred. The function returns the amount actually transferred,
   *  in case of a fee. On success, the cToken holds an additional `actualMintAmount`
   *  of cash.
   */
  uint256 actualMintAmount = doTransferIn(minter, mintAmount);
 
  /*
   * We get the current exchange rate and calculate the number of cTokens to be minted:
   *  mintTokens = actualMintAmount / exchangeRate
   */
 
  uint256 mintTokens = div_(actualMintAmount, exchangeRate);
 
  /*
   * We calculate the new total supply of cTokens and minter token balance, checking for overflow:
   *  totalSupplyNew = totalSupply + mintTokens
   *  accountTokensNew = accountTokens[minter] + mintTokens
   * And write them into storage
   */
  totalSupply = totalSupply + mintTokens;
  accountTokens[minter] = accountTokens[minter] + mintTokens;
 
  ......
}

3. 攻擊者以直接給 soVELO 合約發送 VELO 代幣的方式,給 soVELO 合約發送了數值為的 2,552,964,259,704,265,837,526 VELO 代幣,此時 soVELO 合約持有的 VELO 代幣增多,但是由於沒有新的 soVELO 代幣的鑄造,因此 totalSupply 保持不變,也就意味著此時根據 exchangeRate 計算公式計算出的 exchangeRate 會變大。

4. 攻擊者將持有的 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。攻擊者使用同樣的手法多次重複攻擊,最後獲利巨大。

具體步驟如下:

1. 攻擊者執行題案中的第五筆交易,設定提案中規定的借貸因子。

2. 攻擊者從 VolatileV2 AMM - USDC/VELO 池子中閃電貸出數值為的 35,469,150,965,253,049,864,450,449  VELO 代幣,這會觸發攻擊者的 hook 函數。在 hook 函數中,攻擊者繼續執行攻擊操作。

3. 攻擊者將自己持有的 VELO 代幣發送給 soVELO 合約,以進一步增加 exchangeRate。目前 soVELO 合約中總共有數值的 35,471,703,929,512,754,530,287,976 VELO 代幣(攻擊者三次轉入的 VELO 代幣和)。

4. 攻擊者創建新的合約 0xa16388a6210545b27f669d5189648c1722300b8b,在建構函數中,將持有的 2 個 soVELO 代幣轉給新創建的合約 0xa163(以下稱為攻擊者 0xa163)。

5. 攻擊者 0xa163 以持有的 soVELO 代幣,從 soWETH 藉出數值為 265,842,857,910,985,546,929  的 WETH。

6. 攻擊者 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,000

根據新的 exchangeRate 計算 redeemTokens  的的值為 1.99,由於 Solidity 向下取整的特性,redeemTokens  的值最終為 1。也就意味著攻擊者 0xa163 使用數值為 1 的 soVELO 代幣,贖回了先前存入的幾乎所有的 VELO 代幣。同時攻擊者 0xa163 也賺取了從 soWETH 借出的數值為 265,842,857,910,985,546,929  的 WETH。 soVELO.redeemUnderlying:

function redeemUnderlying(uint redeemAmount) override external returns (uint) {
    redeemUnderlyingInternal(redeemAmount);
    return NO_ERROR;
}
 
function redeemUnderlyingInternal(uint256 redeemAmount)
    internal
    nonReentrant
{
    accrueInterest();
    // redeemFresh emits redeem-specific logs on errors, so we don't need to
    redeemFresh(payable(msg.sender), 0, redeemAmount);
}
 
function redeemFresh(
    address payable redeemer,
    uint256 redeemTokensIn,
    uint256 redeemAmountIn
) internal {
    ......
  
    /* exchangeRate = invoke Exchange Rate Stored() */
    Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()});
 
    uint256 redeemTokens;
    uint256 redeemAmount;
    /* If redeemTokensIn > 0: */
    if (redeemTokensIn > 0) {
        ......
    } else {
        /*
         * We get the current exchange rate and calculate the amount to be redeemed:
         *  redeemTokens = redeemAmountIn / exchangeRate
         *  redeemAmount = redeemAmountIn
         */
        redeemTokens = div_(redeemAmountIn, exchangeRate);
        redeemAmount = redeemAmountIn;
    }
 
    ......
    /*
     * We write the previously calculated values into storage.
     *  Note: Avoid token reentrancy attacks by writing reduced supply before external transfer.
     */
    totalSupply = totalSupply - redeemTokens;
    accountTokens[redeemer] = accountTokens[redeemer] - redeemTokens;
 
    /*
     * We invoke doTransferOut for the redeemer and the redeemAmount.
     *  Note: The cToken must handle variations between ERC-20 and ETH underlying.
     *  On success, the cToken has redeemAmount less of cash.
     *  doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
     */
    doTransferOut(redeemer, redeemAmount);
 
    ......
}

soVELO.exchangeRateStoredInternal:

function exchangeRateStoredInternal()
    internal
    view
    virtual
    returns (uint256)
{
    uint256 _totalSupply = totalSupply;
    if (_totalSupply == 0) {
        /*
         * If there are no tokens minted:
         *  exchangeRate = initialExchangeRate
         */
        return initialExchangeRateMantissa;
    } else {
        /*
         * Otherwise:
         *  exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
         */
        uint256 totalCash = getCashPrior();
        uint256 cashPlusBorrowsMinusReserves = totalCash +
            totalBorrows -
            totalReserves;
        uint256 exchangeRate = (cashPlusBorrowsMinusReserves * expScale) /
            _totalSupply;
 
        return exchangeRate;
    }
}

7. 攻擊者 0xa163 將藉到的 WETH 和贖回的 VELO 代幣全部轉給了上層攻擊者,然後自毀。

8. 攻擊者呼叫 soWETH 的 liquidateBorrow  函數,用來清算前面新建立的合約 0xa163 借貸的部分資產,目的是拿回鎖定住的數值為 1 的 soVELO 代幣。目前攻擊者只持有數值 1 的 soVELO 代幣。

9. 攻擊者呼叫 soVELO 的 mint  函數,再一次抵押鑄造 soVELO 代幣,目的是湊夠數值為 2 的 soVELO 代幣,然後再次執行上述第 3-8 步,獲利其他的 undeylying token。

10. 攻擊者執行數次第 9 步的操作,還掉閃電貸,獲利離場。

$100 如何撬動 $650 萬

攻擊發生後,X 上 @tonyke_bot 用戶在交易 0x0a284cd 中,透過抵押 1144 個 VELO 代幣到 soVELO 合約中,鑄造了 0.00000011 個 soVELO。這樣操作之所以能夠阻止攻擊者進一步攻擊,是因為這筆交易改變了 soVELO 中 totalSupply 的大小和持有的 VELO 代幣的數量 totalCash,而 totalSupply 增長對於計算 exchangeRate 產生的影響大於 totalCash 增長產生的影響,因此 exchangeRate 變小,從而導致攻擊者進行攻擊時,無法再利用精確度損失賺取 soVELO,導致攻擊無法再進行。

圖片

資金追蹤

攻擊者攫取非法收益後不久便將資金進行了轉移,大部分資金轉移到了以下 4 個地址當中,有的是為了換個地址繼續攻擊,有的是為了洗錢:

  1. 0x4ab93fc50b82d4dc457db85888dfdae28d29b98d

攻擊者將 198 WETH 轉入了該地址,然後該地址採用了相同的攻擊手法,在下列交易中獲得非法收益:

soToken攻擊交易非法收益價值
soUSDCnative0xb43748ed668c1e44cf0a3e829ca0fe24eceaee7d33d06072bb11ca99afa7f4481264790.21 USDC
soUSDT0x67632758ce19a41648f28fba6d3d87c7248202238234137bb4741da097cfa029777632.56 USDT
sowstETH0x100ff0317251b899fe50baea8d0a107dd2fa9dd3b85dcff66f9841f25dfd1a4d1667.45 wstETH
soWBTC0x5729810d3cc325597be4f9c1a1c5d03ebad02c55750b966732ed6ab628a8f72239.27 WBTC
soWBTC0xfea20e78a94b3a34951031e3e6fc71f27c348eb7116dc24a777f46e6fe5a81f624.66 WBTC
soWBTC0x150a8ad61cdf9ba5a687b9cff95c90aa5eaa825ac1b07c016ccb905f97d511c398.99 WBTC

攻擊結束後,該地址將上述非法所得轉給了 0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb。

  1. 0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb

攻擊者將 724277 USDC、2353 VELO 轉入了該地址,並將 USDC 兌換了 Ether。隨後立即將部分資金轉入了 Stargate 跨鏈橋,剩下大部分非法資金殘留在該地址中

跨鏈洗錢相關交易 hash
0x1c91e9491c08fc8e27563767df1e1df33362db9915654edf02ff54a966ef792b
0x6fe22af1d530492ac6cda1d576d606dbefecf1987ba48951621aa03d5ca8ff45
0xc1d37f8bb17f699a8231c5e84c3fbaca124fcdd2b22e1906364db1ef9d4c72cd
0x291de2fbec9f8b726d4099847a5fa5c7d9fe6da46c6666edd5813d23d00bad47
  1. 0xbd18100a168321701955e348f03d0df4f517c13b 

攻擊者將 33 WETH 轉入了該地址,並採用 peel chain 的方式嘗試洗錢,洗錢鏈路如下:

0xbd18100a168321701955e348f03d0df4f517c13b  ->  0x7e97b74252b6df53caf386fb4c54d459cb6928 -b6df53caf386fb4c54d459cb6928  ->>  a505321  2b4 ->  0x9f09ec563222fe52712dc413d0b7b66cb5c7c795

  1. 0x4fac0651bcc837bf889f6a7d79c1908419fe1770

攻擊者將 563 WETH 轉入了該位址,隨後轉給了 0x1915F77A116dcE7E9b8F4C4E43CDF81e2aCf9C68,目前沒有進一步行為。

攻擊者這次洗錢的手段相對來說較為專業,手法呈現多元趨勢。因此對於我們 Web3 參與者來說,在安全方面要持續不斷地提高我們的反洗錢能力,透過 KYT、AML 等相關區塊鏈交易安全產品來提高 Defi 專案的安全性。

安全建議

  1. 精度損失需重視。精度損失導致的安全問題層出不窮,尤其是在 Defi 專案中,精度損失往往導致嚴重的資金損失。建議專案方和安全審計人員仔細審查專案中存在精確度損失的程式碼,並做好測試,盡量規避漏洞。
  2. 建議類似於 Compound 中 cToken 這種 market 的創建和首次抵押鑄造操作由特權使用者來執行,避免被攻擊者操作,從而操作匯率。
  3. 當合約中存在關鍵變數依賴或  this.balance  的 token.balanceOf()  值時,需要慎重考慮該關鍵變數改變的條件,例如是否允許直接透過給合約轉原生幣或代幣的方式來改變該變數的值,還是只能透過呼叫某特定函數才能改變該變數的值。

免責聲明:作為區塊鏈資訊平台,本站所發布文章僅代表作者及來賓個人觀點,與 Web3Caff 立場無關。文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。