NFT liquidity solver XCarnival was exploited with a total loss of 3087 ETH
Here’s a brief analysis of this incident:
- There’s a bug in the NFT platform: After you withdraw your collateralized NFT, its orderID is still there available for loan requests.
- Three contracts are involved: xETH, stores funds; xNFT, NFT manager; P2Controller, the checker for many lending restrictions.
- Hacker funded his account from Tornado. Then bought #BAYC 5110 from OpenSea.
- He deployed a Master contract 0xf70f691d30ce23786cfb3a1522cfd76d159aca8d, which derived many Slave contracts as sybils to use the same NFT for borrowing, eg. 0x53386a82e55202a74c6d83c7eede7a80ba553714
- First, the Master transferred BAYC 5110 to the Slave(eg, 0x5338…). Slave then called pledgeAndBorrow() function in xNFT, with the BAYC and borrowed nothing(DIY fake xToken and 0 amount). In this step, an orderID (43) was generated.
- All can be seen in this Tx, but only in internal txs. You must dive into the rabbit hole to analyse the call stack if you want a complete view. https://etherscan.io/tx/0x3d0dd72364eccf2c31275e4703426e4c1779fb152841cf6f916a681e49e832e5
7. Then Slave 5338 withdrew the NFT and sent it back to Master, who then repeated this process with other Slaves. In this way they created many orderIDs, which can later be used as lending credentials. But bugged xNFT contract didn’t revoke the credential after withdrawing.
8. So next step the Master called all Slaves, in turn, to borrow $ETH from xETH contract. Attack completed. The hacker borrowed money from void(collateral NFT had already been withdrawn). One of the tx:https://etherscan.io/tx/0xabfcfaf3620bbb2d41a3ffea6e31e93b9b5f61c061b9cfc5a53c74ebe890294d
9. The above is the big picture. Let’s dive deep into some details. In xNFT contract, withdrawNFT() won’t nullify the orderId after withdraw. So by the time the P2controller calls getOrderDetail(), the order is still valid.
10. In xETH, borrow() will call borrowInternal() then controller.borrowAllowed() to verify if an orderId is valid.
11. Here is the borrowAllowed() in P2controller. It will first ask xNFT.getOrderDetail(). There are many other restrictions, but none of them can stop the hacker. Note: the reason the hacker needed multiple slaves is there is an amount checker for a single order at the bottom.
Summary: Collateral is still valid after withdrawing. This is a very simple & naive bug in contract implementation. The following pic is the clear call stack in those intertwined internal transactions. It could help if you want to analyze without tools.