berwinYes

Posted on Apr 24, 2023Read on Mirror.xyz

复盘aishiba发币漏洞——项目方的低级bug,科学家的又一次狂欢

AIshiba作为arb上一个比较早期的土狗NFT,毫不夸张的说引燃了arb上的NFT潮流。floor price曾一度最高达到0.04e,但是现在已经跌倒了0.004e(大概8刀)。

这就是妥妥的土狗项目,官网做的也是极其简陋,连白皮书都没有,只有路线图。但是没办法,赶上了风口,猪都能飞上天,赶上arb的aidog热度。

并且在今天凌晨进行nft空投发币,空投规则是按照快照时的地址NFT个数计算,即NFT拥有数量越多,空投claim的代币越多,nft空投上限10个,超过按照10个计算。

谁知道发币出现bug,导致科学家发现漏洞,单号直接mint最大量的token 17700000000000 * 10 ** 6 。最高点卖出大概1e+。 代币k线

核心原因是项目方利用Merkle Tree发放 空投token,但是本应该在后台的保存了全量地址被项目方放在了前端,并且未做任何混淆代码的保护处理。导致proof被轻易计算得到。(了解原理可参阅文末参考文献)

只需要在官网按 F12 整个代码一览无余:

merkleProof.js

const { WHITELIST_ADDRESS } = require("./whitelist_Address")
const keccak256 = require("keccak256")
const { MerkleTree } = require("merkletreejs")


const addressLeaves = WHITELIST_ADDRESS.map(x => keccak256(x))
const merkleTree = new MerkleTree(addressLeaves, keccak256, {
    sortPairs : true
})

const rootHash = merkleTree.getHexRoot()

module.exports = { merkleTree, rootHash }

并且调用合约的方法,传入参数也非常清晰:

index.js
 
const handleClaimForNfts = async () => {
    try {
      const signer = await getProviderOrSigner(true);
      const userAddress = signer.getAddress();
      const aiShibaContract = new Contract(
        AISHIBA_CONTRACT_ADDRESS,
        AISHIBA_CONTRACT_ABI,
        signer
      );
 
    
      const txn = await aiShibaContract.claimTokensForNft(merkleProof, nftBalance);
      await txn.wait();
      

      console.log("txn", txn);
      console.log("txn", "successful");

    } catch (error) {
      console.error(error);
    }
  };

可以看到调用了合约的 claimTokensForNft() 方法,传入了merkleProofnftBalance 两个变量,nftBalance 代表着快照拥有NFT的数量。

其中merkleProof 只需要 用下列方法 merkleTree.getHexProof(_leaf) 即可获取

index.js

const getProof = async () => {
    const signer = await getProviderOrSigner(true);
    const _userAddress = await signer.getAddress();
    const _leaf = keccak256(_userAddress);
    const _merkleProof = merkleTree.getHexProof(_leaf);
    setUserAddress(_userAddress);
    setMerkleProof(_merkleProof);
  };

再看一下领取的合约代码

    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);
    }

_amount 即为nft的数量,合约按照 _amount 数量进行空投token。

  • _amount<=2 空投代币 1475000000000 * 10 ** 6

  • 2<_amount<=4 空投代币 3375000000000 * 10 ** 6

  • 4<_amount<=9 空投代币 7750000000000 * 10 ** 6

  • _amount>=10 空投代币 17700000000000 * 10 ** 6

合约中没有对_amount的校验操作,即使你的钱包地址拥有1个nft(即拥有白名单权限),只要Merkle tree 验证成功,_amount传入10或者更大值也能mint 到单地址最高数量代币,即 17700000000000 * 10 ** 6 。

大概有1800个地址领取了空投,绝大部分都是科学家

项目方发现了不对劲,在凌晨的00:45已经关闭了合约,并在接下来转走了合约中所有token。

据传言(道听途说):背后的项目方是大学生,整个寝室一起出动一起赚钱,如果真是这样,现在这大学生还真他娘的敢想敢干,割起韭菜来也是毫不手软。

可惜 螳螂捕蝉,黄雀在后,项目成了科学家的提款机,最可怜的还是那些真金白银在DEX和CEX中买Token的韭菜们。

不玩土狗的我 看着大佬们的卖币截图和聊天记录,又一次感慨道:

别人赚钱如呼吸般简单,自己赚钱如吃屎般困难。

本人推特:@coolberwin_eth

文章参考:

Merkle tree的 原理: WTF Ethers极简入门: MerkleTree脚本