# Team HW6 # [NBLGAME](https://app.blocksec.com/explorer/security-incidents?page=1&pageSize=200&sort=1) Expolit Analysis # Team Member ![image](https://hackmd.io/_uploads/rkgtWOpEyx.png =40%x)![image](https://hackmd.io/_uploads/H13cWO6N1l.png =40%x) ![image](https://hackmd.io/_uploads/By3hWu6Ekx.png =40%x)![image](https://hackmd.io/_uploads/BJmRWOaN1l.png =40%x) ## NBLGAME 協議介紹 ### 合約組成 - [NBL ERC20](https://optimistic.etherscan.io/token/0x4b03afc91295ed778320c2824bad5eb5a1d852dd) - [NBL Box ERC721](https://optimistic.etherscan.io/token/0x534e1a8a89548c44be7aba1c3c27951801940c10) - [NblNftStake](https://optimistic.etherscan.io/address/0x5499178919c79086fd580d6c5f332a4253244d91) - [Stakebook](https://optimistic.etherscan.io/address/0x770b279678105b378f90055aad1b22d2a0306baf) ### 協議功能 NBLGAME 協議主要實現 NFT 和 FT 的質押功能,允許用戶將擁有的 NFT 和 FT 存入合約中,參與質押以獲取獎勵。 用戶可以將 `NBL FT` 和 `NBL Box NFT` 存入 `NblNftStake` 合約中,合約會根據質押的 NFT 等級和 FT 數量去計算獎勵,最後調用 `Stakebook` 合約去給予用戶 `NBL FT` ## 駭客如何利用漏洞點攻擊 ### 使用到的合約 - [Entry point contract](https://optimistic.etherscan.io/address/0xe4d41bdd6459198b33cc795ff280cee02d91087b) - [Attack logic contract](https://optimistic.etherscan.io/address/0xfc3b08555b1c328ecf8b8a0ccd85679bf59bba4c) - [UniswapV3](https://optimistic.etherscan.io/address/0xfaf037caafa9620bfaebc04c298bf4a104963613#code) ### 漏洞利用方式 `NblNftStake`合約中的 ==withdrawNft()== function 裡調用到 NBL Box ERC721 的 ==safeTransferFrom()==,==safeTransferFrom()== 在 receiver 是合約的話會去調用 ==onERC721Received()== 做檢查,駭客在 ==withdrawNft()== 沒做 reentrancy 防護的狀況下寫了一份惡意合約作為 receiver,而在 ==onERC721Received()== 中再次調用 ==withdrawNft()== 進行重入攻擊,由於 StakeInfo 的狀態變數還未改變,因此可以再次通過 require 的檢查,而讓 `NblNftStake` 重複執行了兩次 NBL ERC20 的 transfer,成功移轉合約內的所有資金給惡意合約 ## Transaction trace [Tenderly](https://dashboard.tenderly.co/tx/optimistic/0xf4fc3b638f1a377cf22b729199a9aeb27fc62fe2983a65c4d14b99ee5c5b2328?trace=0.2.1.3.10.1.0.1.3) ### 攻擊前置作業 ![hpa-026269](https://hackmd.io/_uploads/Bk1-Yq671g.png) ### 調用 vulnerable function ![upload_d43647389992c4d10cf072b0cf7c595f](https://hackmd.io/_uploads/rJ4Idoa7kg.png) ### 將竊取的 token 換成其他資產 ![upload_5ef8ef39f54fad1e83bab0dfe849cd2c](https://hackmd.io/_uploads/S1LlYoTQJl.png) ## 如何做 POC 重現 ### [POC GitHub](https://github.com/CT77777/NBLGAME-exploit-analysis) ### 合約調用流程 ```mermaid graph TD Z[攻擊合約] --> A[UniswapV3Pool - NBL_USDT.flash] -->|1.借出 NBL| B[攻擊合約 - uniswapV3FlashCallback] B -->|2.解鎖槽位| C[NblNftStake - NblNftStake.unlockSlot] C -->|3.存入 NFT| D[NblNftStake - NblNftStake.depositNft] D -->|4.存入 NBL| E[NblNftStake - NblNftStake.depositNbl] E -->|5.提取 NFT| F[NblNftStake - NblNftStake.withdrawNft] F -->|6.轉移 NFT| G[NFT合約 - nft.safeTransferFrom] G -->|7.觸發回調| H[攻擊合約 - onERC721Received] H -->|8.轉回 NFT| I[NFT合約 - NBF.transferFrom] I -->|9.重入攻擊| J[NblNftStake - NblNftStake.withdrawNft] J -->|10.再次存入| K[NblNftStake - NblNftStake.depositNft] K -->|11.歸還閃電貸| L[NBL - NBL.transfer] L -->|12.完成| M[UniswapV3Pool - Repay Flash Loan] %% 顏色標記 classDef flashloan fill:#f9f,stroke:#333,stroke-width:2px classDef attack fill:#f96,stroke:#333,stroke-width:2px classDef normal fill:#fff,stroke:#333,stroke-width:2px %% 應用樣式 class A,B,L,M flashloan class F,G,H,J attack class C,D,E,I,K normal ``` ## 優化方法 1. 正確使用 ReentrancyGuard 2. 遵循 CEI(Checks-Effects-Interactions)模式: ```solidity function withdrawNft(uint256 _index) public nonReentrant { // 1. Checks StakeInfo[] storage stakes = userStakeInfo[msg.sender]; require(_index < stakes.length, "invalid stake index"); uint tokenid = stakes[_index].nftTokenId; require(tokenid > 0, "no stake available"); uint amount = stakes[_index].nblStakeAmount; uint power = getSlotPower(msg.sender, _index); uint beginTimestamp = stakes[_index].begin; uint inscriptionId = stakes[_index].inscriptionId // 2. Effects (先更新狀態) stakes[_index].nftTokenId = 0; stakes[_index].inscriptionId = 0; stakes[_index].nblStakeAmount = 0; stakes[_index].begin = 0; // 3. Interactions (最後進行外部調用) nft.safeTransferFrom(address(this), msg.sender, tokenid); if (inscriptionId > 0) { inscription.safeTransferFrom( address(this), msg.sender, inscriptionId ); } uint discount = calcDiscount(beginTimestamp, amount); nbl.safeTransfer(community, discount); nbl.safeTransfer(msg.sender, SafeMath.sub(amount, discount)); uint multiply = slotPowerMultiplies[stakes.length - 1]; power = SafeMath.mul(power, multiply) / 100; stakebook.withdraw(msg.sender, power); emit WithdrawNft(msg.sender, tokenid); } ```