# DeFi Hacked Analysis: Xcarnival Logic Flaws

By [{     }](https://paragraph.com/@whiteberets) · 2023-01-31

---

在 2022Q2，因緣際會開始拜師學習 Solidity，意外的發現自己對於 DeFi Security 蠻有熱誠，也向大佬學習了很多技術和心態。

在 2023 年，我想完成的其中一件事是把 ventral.digital 的這篇 [Ethereum Smart Contract Auditor's 2022 Rewind](https://ventral.digital/posts/2022/12/15/ethereum-smart-contract-auditors-2022-rewind) 所提到的攻擊事件進行完整的分析，也順便當作自己的筆記。

隨便數了一下，內文大概提到了超過 80 個攻擊事件吧，如果每一個攻擊事件都進行分析、寫 Reproduce，大概會耗掉兩個月時間來練功，希望可以堅持住，哈哈。

我還沒仔細看每一個攻擊事件，打算直接一邊分析一邊當作鐵人賽寫文章了，太類似的攻擊事件或屬於無法寫 Reproduce 的漏洞(像 BNB Chain hacked 那樣?)，應該會直接跳過。文章撰寫上沒有順序，對什麼攻擊事件感興趣就寫什麼，不過每個大類別至少都會分析一個吧。

Intro
-----

[XCarnival](https://xcarnival.fi/) 是一個在以太坊上的去中心化 NFT 市集，用戶可以在該平台上買賣 NFT、將 NFT 進行超額抵押貸款等操作。

![XCarnival 官方網站頁面](https://storage.googleapis.com/papyrus_images/6ba5ee9e0540d34771cb320ea9d19869df990405d8f7f9bcd0b4fe9f3ea9d984.png)

XCarnival 官方網站頁面

在 2022 年 06 月 26 日，該項目遭到駭客攻擊，駭客利用了該項目智能合約所存在的邏輯漏洞，該漏洞使駭客將超額抵押的 NFT 解除抵押後，仍能進行借款，這可對系統造成壞帳，最終 XCarnival 項目方損失了 3087 顆 ETH，當時市價相當於 3.87 Million US$。

攻擊者地址：`0xb7cbb4d43f1e08327a90b32a8417688c9d0b800a`

主要攻擊合約：`0xf70f691d30ce23786cfb3a1522cfd76d159aca8d`

被攻擊的 XNFT.sol 合約：`0x39360ac1239a0b98cb8076d4135d0f72b7fd9909`

Details
-------

那駭客是如何利用這個漏洞盜取 3087 ETH 呢？

我做了一張圖來分解駭客的攻擊流程，就讓我們搭配這張圖一步步食用吧：

![攻擊流程](https://storage.googleapis.com/papyrus_images/a4a2edcb2271e45cf62c5ca1504dbde0575619f194c669a68769599386a280ad.jpg)

攻擊流程

### 1\. Deploy

攻擊者在區塊高度 15028839 部署了主要攻擊合約，合約代碼當然沒有開源，不過我們可以從 Reproduce 中大致理解攻擊合約的代碼邏輯。

TxID: [0xe4f99b2fb86a317eb16f7f288fda74ab07f0ffcbf645fb3b1a6490ca23206d09](https://etherscan.io/tx/0xe4f99b2fb86a317eb16f7f288fda74ab07f0ffcbf645fb3b1a6490ca23206d09)

### 2\. Transfer BAYC#5110 to Main Attack Contract

攻擊者將它從區塊高度 [15028718](https://etherscan.io/block/15028718) 買來的 BAYC#5110 轉給 Main Attack Contract，準備作為 XCarnival 超額抵押貸款的抵押品。

順帶一提，攻擊者的成本是 91.65 ETH (114K US$)，有錢人才玩得起的漏洞。

*   賣家收到 87.0675 ETH (95%)
    
*   藝術家版稅 2.29125 ETH (2.5%)
    
*   OpenSea服務費 2.29125 ETH (2.5%)
    

![買 BAYC#5110 的 Tx Details](https://storage.googleapis.com/papyrus_images/705eef660b97ee05ac82fb0504321cb64078888a4d87697446c73c5ca67472a8.png)

買 BAYC#5110 的 Tx Details

買在進到下一步之前，稍稍讓我們整理一下目前為止的步驟：

1.  區塊高度 [15028718](https://etherscan.io/tx/0x16bb7799cf4e919bcb81f3ed531743ea6a6857e9a5121500fa1e3619bb2b82cf)：買 BAYC#5110
    
2.  區塊高度 [15028839](https://etherscan.io/tx/0xe4f99b2fb86a317eb16f7f288fda74ab07f0ffcbf645fb3b1a6490ca23206d09)：部署 Main Attack Contract
    
3.  區塊高度 [15028847](https://etherscan.io/tx/0x7cd094bc34c6700090f88950ab0095a95eb0d54c8e5012f1f46266c8871027ff)：把 BAYC#5110 轉給 Main Attack Contract
    

對應 [txs](https://etherscan.io/txs?a=0xb7cbb4d43f1e08327a90b32a8417688c9d0b800a) 最底下 3 條交易紀錄：

![攻擊者的 txs 紀錄](https://storage.googleapis.com/papyrus_images/d7d8629afae0f6de95ee96fc1743e2b416a82bec34430e79f48f0c9b6633d5f0.png)

攻擊者的 txs 紀錄

### 3~8. Deploy Payload Contracts

從上圖 _攻擊者的 txs 紀錄_ 你可以觀察到，攻擊者在轉給主攻擊合約 BAYC#5110 後，一直對主攻擊合約`0xf70f691d30ce23786cfb3a1522cfd76d159aca8d` 調用 `0xadf6a75d` 這個 Function。

那這到底是在幹嘛？我們可以透過 Blocksec Phalcon 進行 Transaction View：

![第一個交易, TxId: 0x422e7b0a449deba30bfe922b5c34282efbdbf860205ff04b14fd8129c5b91433](https://storage.googleapis.com/papyrus_images/c590bc368595d8e15db8e9c6393646456ef9cdedb965ec0e1bbfa8ab5e646108.png)

第一個交易, TxId: 0x422e7b0a449deba30bfe922b5c34282efbdbf860205ff04b14fd8129c5b91433

將調用層級設為 `Expand: 1`，可以觀察到基本上主攻擊合約做的就是幾件事：

1.  部署暫時的 Payload Contract
    
2.  把 BAYC#5110 從主攻擊合約，轉移到 Payload Contract
    
3.  呼叫 Payload Contract 的 `0x97c1edd3` 函數
    

那 `0x97c1edd3` 函數在做什麼呢？將它展開至第 2 層：

![函數 0x97c1edd3 的調用流程](https://storage.googleapis.com/papyrus_images/ad104368c8ef05a87ed0c934d399768f5bcab21aeac84a3a9063e65a78d5f830.png)

函數 0x97c1edd3 的調用流程

`STATICCALL` 都是向主攻擊合約讀取狀態變數，所以基本上我們可以先略過，專注看 `CALL` 與 XCarnival 互動的部分。

1.  `setApprovalForAll()` 給 XCarnival 合約權限，讓 XCarnival 可以把 BAYC NFT 轉走
    
2.  `pledgeAndBorrow()` 把 BAYC#5110 作為抵押品向 XCarnival 借錢
    
3.  `counter()` 取得剛剛抵押借款的 orderId，這邊是取到 `orderId: 11`
    
4.  `withdrawNFT()` 把剛剛抵押的 BAYC#5110 解除抵押，將 NFT 退還。
    
5.  `transferFrom()` 把 BAYC#5110 再轉回給主攻擊合約
    
    這個 Transaction 重複 1 ~ 5 四次，然後用相同的手法重複了 14 個 Transactions，建立了 `orderId: 11` 到 `orderId: 66`。
    

還是覺得很頭痛嗎？沒關係，這邊有 [Reproduce Code](https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/XCarnival.exp.sol#L91-L103) 幫助你梳理攻擊者的邏輯：

![Reproduce Code of 0x97c1edd3 Function](https://storage.googleapis.com/papyrus_images/7d45c8115a43aaa729af6df2953287886027835f39b430ff822487a616c42394.png)

Reproduce Code of 0x97c1edd3 Function

> `0xb14b-TransparentUpgradeableProxy`地址，其實是有漏洞的 `XNFT.sol` 的透明代理地址，有學過 Proxy Pattern 的你，知道 XNFT.sol 的狀態變數都是儲存在 0xb14b-TransparentUpgradeableProxy 的，所以使用者其實是在和 0xb14b-TransparentUpgradeableProxy互動。

對 DeFi Landing 稍微暸解過的你，可能會覺得很疑惑：為什麼攻擊者可以 `pledgeAndBorrow()` 之後，不還款就直接調用 `withdrawNFT()` 呢？

這其實是因為攻擊者 `pledgeAndBorrow()` 借出 0 元，所以只是”單純的”建立了這筆抵押紀錄 `orderId`。

並且合約代碼也沒有不允許債務等於 0 的訂單取消抵押 NFT：

![L260: 允許債務為 0 的 order 提取 NFT](https://storage.googleapis.com/papyrus_images/31eef122ef025bd31df8c25e406f75e6e9d0bed3fffa1c8b1af5266b65c4e843.png)

L260: 允許債務為 0 的 order 提取 NFT

當順利通過 `withdrawNFT()` 的所有檢查，也順利取回 BAYC#5110 了，就會設定狀態變數`_order.isWithdraw = true`。

所以攻擊者在一個 Transaction，就完成了 3 ~ 8 的步驟，分別是：

*   Deploy Payload Contract
    
*   Transfer BAYC#5110 from Main to Payload
    
*   Call: `XNFT.pledgeAndBorrow()` 將 BAYC#5110 作為抵押品，借 0 元
    
    *   Transfer BAYC#5110 from Payload to XNFT
        
*   Call: `XNFT.withdrawNFT()` 取回 BAYC#5110
    
    *   Transfer BAYC#5110 from XNFT to Payload
        

### 9~13. Exploit The Vulnerability

好的，目前我們已經知道攻擊者把 BAYC#5110 作為抵押品送到 XCarnival 裡，借款 0 元同時產生 order，然後將 BAYC#5110 解除抵押從 XCarnival 退回。

接下來我們來分析攻擊者究竟是怎麼把 ETH 盜領出來的。

![第一個 start() 交易, TxId: 0xabfcfaf3620bbb2d41a3ffea6e31e93b9b5f61c061b9cfc5a53c74ebe890294d](https://storage.googleapis.com/papyrus_images/7d28aef870c2e2ef0049f4f85a1480304480d76b6386b201d88df5b9fb725802.png)

第一個 start() 交易, TxId: 0xabfcfaf3620bbb2d41a3ffea6e31e93b9b5f61c061b9cfc5a53c74ebe890294d

可以看到攻擊者先調用了主攻擊合約的 `start()` 函數。

主攻擊合約再呼叫每一個 Payload Contract 的 `0x2a3e7cec` 函數。

Payload Contract 再向 XCarnival 的 `XToken.sol` 互動，調用 `borrow()` 函數將 36 ETH 借出來，漏洞利用結束。

由於在先前步驟，攻擊者已經透過調用 `withdrawNFT()` 解除抵押了 BAYC#5110，所以 BAYC#5110 實際上是在攻擊者手上，所以此時 `borrow()` 出來的 36 顆 ETH 沒有實質資產儲備，造成 XCarnival 壞帳，最終攻擊者反覆操作，盜走 3087 顆 ETH。

### 漏洞根本原因分析 - 為什麼在 BAYC#5110 解除抵押的狀況下，XCarnival 可以讓攻擊者憑空借出 36 顆以太幣？

讓我們看一下 `XToken.sol` 的代碼：

![XToken.sol - borrow() 函數](https://storage.googleapis.com/papyrus_images/f260f620d2f65da468c5a6c8f783a5695194138fbc3e6393faa8d0391c1c679c.png)

XToken.sol - borrow() 函數

如果你對 DeFi 不陌生的話，其實可以發現 `XToken.sol` 似乎是 Fork Compound 的代碼。

讓我們跟進 `controller.borrowAllowed()` 函數，看看風控模組在確定借款之前，做了什麼校驗：

![controller.borrowAllowed()](https://storage.googleapis.com/papyrus_images/58a9c27eec5d99ad2a9d360ce93a5b76ab2e7874b8c53096ab5582f7ad510052.png)

controller.borrowAllowed()

從相關的 require 語句可以觀察到，`controller.borrowAllowed()` 主要是檢查 BAYC 系列是否為 XCarnival 支持超額抵押貸款的 NFT 系列。

上圖可以看到，又出現一個 `orderAllowed(orderId, borrower)`，所以我們繼續往下追：

![controller.orderAllowed()](https://storage.googleapis.com/papyrus_images/cf1e43c179e6090046a818fc99dfe2c7d0e7ec3344116e9be6e5668e72841d05.png)

controller.orderAllowed()

在 L49，controller 向 xNFT 合約索取有關 orderId 對應的 NFT 系列地址與抵押者，require 的要求是借款人調用 `borrow(orderId)` 的 orderId 是合法的、借款人就是抵押人、抵押品不能是已經被清算的狀態。

![getOrderDetail() 也只是簡單的返回狀態變數](https://storage.googleapis.com/papyrus_images/a392d20b3c9d044a6f63ba7ca4fbc469c458eb4eafafa349e98f18d8664e0616.png)

getOrderDetail() 也只是簡單的返回狀態變數

神奇的地方在於 `orderAllowed()` **函數並沒有檢查** `orderId.isWithdraw` **是否等於 false**，這就讓攻擊者可以拿已經 withdraw 的 orderId 來向 XCarnival 借款！

攻擊者的思路就是利用這個漏洞，**透過多個 Payload Contract 來產生已經 withdraw 過的 orderId，然後反覆向 XCarnival 借款**，最後就造成損失 3087 ETH 的悲劇了。

### 我們可以如何修復漏洞？

現在，我們已經知道漏洞成因、攻擊者的利用思路，現在我們試著寫 Patch：

![文字版: https://pastebin.com/t5E5fAaF](https://storage.googleapis.com/papyrus_images/c42dc63470a940c9a834fbcaaf80a9e695a5ac911b82dfa37f8e2ce850565186.png)

文字版: https://pastebin.com/t5E5fAaF

Summary
-------

Compound 是專注在 ERC20 操作的協議，項目方方 fork 代碼做成 ERC-721 通用的協議還是要仔細檢查兩種協議之間商業邏輯的差異；ERC-721 確實還蠻多坑的，比方說[不怎麼 safe 的 safeMint()](https://github.com/SunWeb3Sec/DeFiVulnLabs/blob/main/src/test/Unprotected-callback.sol)。

在此攻擊事件發生後，我未找到 XNFT.sol 和 XToken.sol 所開源出來的 Code Repo，也沒有找到相關的審計報告，這表明項目方在 Production 上線前似乎未經過第三方代碼安全公司審計，也再次凸顯了審計的重要性。

在 2022 年 7 月 25 日，XCarnival 給 PeckShield 再次審計過，審計報告[連結](https://github.com/peckshield/publications/blob/master/audit_reports/PeckShield-Audit-Report-XCarnival-v1.0.pdf)。

---

*Originally published on [{     }](https://paragraph.com/@whiteberets/defi-hacked-analysis-xcarnival-logic-flaws)*
