# APE 空投合约代码详解

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-03-22

---

上周 BAYC 发币，引起了一阵热度。这篇文章，就来看看 [APE 空投合约](https://etherscan.io/address/0x025c6da5bd0e6a5dd1350fda9e3b6a614b205a1f#code)的代码。

### 规则

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

![APE 空投规则](https://storage.googleapis.com/papyrus_images/da6dd116443f31daccc404e3b34c8e47c725db107ab4da8fc36a213a2a303edd.png)

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 实时持仓来进行空投，而不是在某一个区块或时间点进行快照。而这一点恰恰造成了套利的机会，有科学家根据这个特点进行[闪电贷套利](https://blocksecteam.medium.com/the-short-analysis-of-the-flashloan-attack-to-the-ape-airdrop-490a7d6a1479)，获取了大量的 ETH。

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/ape)*
