xyyme.eth

Posted on Mar 22, 2022Read on Mirror.xyz

APE 空投合约代码详解

上周 BAYC 发币,引起了一阵热度。这篇文章,就来看看 APE 空投合约的代码。

规则

先来看看官网上空投的规则:

APE 空投规则

注意到,必须拥有 BAYC 或者 MAYC 才能够领取空投。而仅仅拥有 Kennel Club ,是不能够领取的,必须搭配前两者才能领取。

代码

接下来看看代码。

数据结构:

// APE 币
IERC20 public immutable grapesToken;

// BAYC nft
ERC721Enumerable public immutable alpha;
// MAYC nft
ERC721Enumerable public immutable beta;
// Kennel Club nft
ERC721Enumerable public immutable gamma;

// BAYC可以领取的数量:10094
uint256 public immutable ALPHA_DISTRIBUTION_AMOUNT;
// MAYC可以领取的数量:2042
uint256 public immutable BETA_DISTRIBUTION_AMOUNT;
// Kennel Club可以领取的数量:856
uint256 public immutable GAMMA_DISTRIBUTION_AMOUNT;
// 总共领取了多少token
uint256 public totalClaimed;

// 领取时间:7776000秒 -> 90 天
uint256 public claimDuration;
// 开始领取时间:2022-03-17 20:08:07(北京时间)
uint256 public claimStartTime;

// 记录这些 nft id 是否被领取过
// BAYC
mapping (uint256 => bool) public alphaClaimed;
// MAYC
mapping (uint256 => bool) public betaClaimed;
// Kennel Club
mapping (uint256 => bool) public gammaClaimed;

这些数据都是在构造方法中直接赋值。

下面的方法计算用户可以领取的数量:

// 计算可以领取的数量
function getClaimableTokenAmount(address _account) public view returns (uint256) {
    uint256 tokensAmount;
    (tokensAmount,) = getClaimableTokenAmountAndGammaToClaim(_account);
    return tokensAmount;
}

function getClaimableTokenAmountAndGammaToClaim(address _account) private view returns (uint256, uint256)
{
    // 计算可以领取token的BAYC有效数量
    uint256 unclaimedAlphaBalance;
    for(uint256 i; i < alpha.balanceOf(_account); ++i) {
        uint256 tokenId = alpha.tokenOfOwnerByIndex(_account, i);
        // 如果该id已经领取过,则跳过
        if(!alphaClaimed[tokenId]) {
            ++unclaimedAlphaBalance;
        }
    }
    // 计算可以领取token的MAYC有效数量
    uint256 unclaimedBetaBalance;
    for(uint256 i; i < beta.balanceOf(_account); ++i) {
        uint256 tokenId = beta.tokenOfOwnerByIndex(_account, i);
        // 如果该id已经领取过,则跳过
        if(!betaClaimed[tokenId]) {
            ++unclaimedBetaBalance;
        }
    }
    // 计算可以领取token的Kennel Club有效数量
    uint256 unclaimedGamaBalance;
    for(uint256 i; i < gamma.balanceOf(_account); ++i) {
        uint256 tokenId = gamma.tokenOfOwnerByIndex(_account, i);
        if(!gammaClaimed[tokenId]) {
            ++unclaimedGamaBalance;
        }
    }

    // 我们前面说过,Kennel Club必须搭配BAYC或者MAYC才能领取
    // 仅仅拥有Kennel Club不可以领取
    // 这里就是对这个条件进行计算
    uint256 gammaToBeClaim = min(unclaimedAlphaBalance + unclaimedBetaBalance, unclaimedGamaBalance);
    // 计算出用户可以领取的token总量
    uint256 tokensAmount = (unclaimedAlphaBalance * ALPHA_DISTRIBUTION_AMOUNT)
    + (unclaimedBetaBalance * BETA_DISTRIBUTION_AMOUNT) + (gammaToBeClaim * GAMMA_DISTRIBUTION_AMOUNT);

    // 返回的两个参数分别为:
    // 1.可以领取的token数量
    // 2.与前两种nft搭配的Kennel Club配对的数量
    return (tokensAmount, gammaToBeClaim);
}

用户领取token的方法:

function claimTokens() external whenNotPaused {
    // 校验当前时间在有效时间区间内
    require(block.timestamp >= claimStartTime && block.timestamp < claimStartTime + claimDuration, "Claimable period is finished");
    // 校验用户拥有有效nft
    require((beta.balanceOf(msg.sender) > 0 || alpha.balanceOf(msg.sender) > 0), "Nothing to claim");

    uint256 tokensToClaim;
    uint256 gammaToBeClaim;

    // 根据上面的方法,得到用户可以领取的数量
    (tokensToClaim, gammaToBeClaim) = getClaimableTokenAmountAndGammaToClaim(msg.sender);

    // 更新BAYC的领取数据并发送事件
    for(uint256 i; i < alpha.balanceOf(msg.sender); ++i) {
        uint256 tokenId = alpha.tokenOfOwnerByIndex(msg.sender, i);
        if(!alphaClaimed[tokenId]) {
            alphaClaimed[tokenId] = true;
            emit AlphaClaimed(tokenId, msg.sender, block.timestamp);
        }
    }

    // 更新MAYC的领取数据并发送事件
    for(uint256 i; i < beta.balanceOf(msg.sender); ++i) {
        uint256 tokenId = beta.tokenOfOwnerByIndex(msg.sender, i);
        if(!betaClaimed[tokenId]) {
            betaClaimed[tokenId] = true;
            emit BetaClaimed(tokenId, msg.sender, block.timestamp);
        }
    }

    // 更新Kennel Club的领取数据并发送事件
    uint256 currentGammaClaimed;
    for(uint256 i; i < gamma.balanceOf(msg.sender); ++i) {
        uint256 tokenId = gamma.tokenOfOwnerByIndex(msg.sender, i);
        // 注意这里是根据Kennel Club nft配对数量进行计算
        if(!gammaClaimed[tokenId] && currentGammaClaimed < gammaToBeClaim) {
            gammaClaimed[tokenId] = true;
            emit GammaClaimed(tokenId, msg.sender, block.timestamp);
            currentGammaClaimed++;
        }
    }

    grapesToken.safeTransfer(msg.sender, tokensToClaim);

    totalClaimed += tokensToClaim;
    emit AirDrop(msg.sender, tokensToClaim, block.timestamp);
}

其他的方法例如,管理员转回未领取的token等,都比较简单,这里不再赘述。

总结

APE 的空投合约相对来说比较简单,它有一个特点是根据用户的 NFT 实时持仓来进行空投,而不是在某一个区块或时间点进行快照。而这一点恰恰造成了套利的机会,有科学家根据这个特点进行闪电贷套利,获取了大量的 ETH。