# APE土拍代码分析

By [point](https://paragraph.com/@point) · 2022-05-06

---

纯属个人收集整理资料+揣测理解，我也不是NFT玩家，也没这个资金实力玩NFT。所以，理解上没有专业人士准确。

业务梳理
----

1.  固定305APE价格售卖
    
2.  售卖分为kyc pre-mint(Contributor才行，应该就是白名单吧)、公售（大家抢）、持有无聊猿和变异猿的人可以再mint
    
3.  5.1买的是盲盒，5.2拆盒
    

![operator操作记录](https://storage.googleapis.com/papyrus_images/d58a2f74191644d1c2382f4bdd171a28dcb552acd8238c649a176dfeb9a0b071.png)

operator操作记录

operator也是操作了这些事情，业务理解应该没太大问题

代码注释
----

删掉了一些业务无关、重复、无需理解的代码

代码还算简单，只有通过随机数将土地和nft721的metadata关联起来有点疑问

通过链上操作可以知道tokenurl是在土拍前就设置了，并且再也没有改过，所以不是其他项目那样卖完后通过改url拆盒

项目方也上传了metadatahashes，所以所有nft metadata是事先就准备好了的

链上的随机数并没有参与链上业务的处理，比如通过随机数+tokenid获取metadata url

既然链上的方法都没有使用，那么这个关联操作应该是放在了链下，

api.otherside.xyz/lands/27893 访问这个地址后，通过他们未开源的算法，将27893+随机数指向了metaurl。但是这样是不是就能够通过修改算法，改变用户的nft了。。。。。。。这点没有想明白，如果有好心人愿意指导下我，可以[zn978740431@gmail.com](mailto:zn978740431@gmail.com)给我发邮件。

网上找到两个还算靠谱的实现文章，看起来都是在链上完成这个操作的，搞不明白ape是怎么想的。因为很多nft项目方在这上面做手脚，还是很想理解透这个事情。

[https://vocus.cc/article/6212f73ffd89780001687585](https://vocus.cc/article/6212f73ffd89780001687585)

[https://zombit.info/write-to-the-nft-project/](https://zombit.info/write-to-the-nft-project/)

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.10;
    
    contract Land  {
    using SafeERC20 for IERC20;
    // attributes
    string private baseURI;
    

![url](https://storage.googleapis.com/papyrus_images/7c14b330a7de2577e3e77db313a4cce12f53d209d34e7881b693114ca42acac9.png)

url

    address public operator;
    //公售开关
    bool public publicSaleActive;
    //公售时间
    uint256 public publicSaleStartTime;
    //荷兰拍用的
    uint256 public publicSalePriceLoweringDuration;
    uint256 public publicSaleStartPrice;
    uint256 public publicSaleEndingPrice;
    //荷兰拍用的
    
    //记录当前的土地数量
    uint256 public currentNumLandsMintedPublicSale;
    //当前mint tokenId
    uint256 public mintIndexPublicSaleAndContributors;
    //ape地址，用于交钱
    address public tokenContract;
    //是否需要验证kyc
    bool private isKycCheckRequired;
    //验证kyc的hash root， Merkle tree验证
    bytes32 public kycMerkleRoot;
    

![kyc hash](https://storage.googleapis.com/papyrus_images/493410298735c77260bf04ea2c6f33947391993fd23bc354443ce54d88c31cbb.png)

kyc hash

    //每次最大可以mint的上限
    uint256 public maxMintPerTx;
    //每个地址总共mint的上限
    uint256 public maxMintPerAddress;
    //每个地址mint的数量
    mapping(address => uint256) public mintedPerAddress;
    //无聊猿和变异猿是否可以额外mint
    bool public claimableActive; 
    bool public adminClaimStarted;
    
    //无聊猿mint的信息
    address public alphaContract; 
    mapping(uint256 => bool) public alphaClaimed;
    uint256 public alphaClaimedAmount;
    //变异mint的信息
    address public betaContract; 
    mapping(uint256 => bool) public betaClaimed;
    uint256 public betaClaimedAmount;
    uint256 public betaNftIdCurrent;
    
    //贡献者mint的信息
    bool public contributorsClaimActive;
    mapping(address => uint256) public contributors;
    uint256 public futureLandsNftIdCurrent;
    
    //未来还可以再mint
    address public futureMinter;
    
    //保证官方没有作弊，用于所有人拿这里面的数据进行验证
    Metadata[] public metadataHashes;
    //随机数seed, 线上结果       0xaa77729d3466ca35ae8d28b3bbac7cc36a5031efdc430821c02bc31a238af445
    bytes32 public keyHash;
    //请求随机数的费用
    uint256 public fee;
    //下面三个获得的随机数然后计算出来的偏移量 线上结果 35117
    uint256 public publicSaleAndContributorsOffset;
    //线上结果 5491
    uint256 public alphaOffset;
    //线上结果 15491
    uint256 public betaOffset;
    //是否请求过随机数
    mapping(bytes32 => bool) public isRandomRequestForPublicSaleAndContributors;
    bool public publicSaleAndContributorsRandomnessRequested;
    bool public ownerClaimRandomnessRequested;
    
    // constants
    uint256 immutable public MAX_LANDS;
    uint256 immutable public MAX_LANDS_WITH_FUTURE;
    uint256 immutable public MAX_ALPHA_NFT_AMOUNT;
    uint256 immutable public MAX_BETA_NFT_AMOUNT;
    uint256 immutable public MAX_PUBLIC_SALE_AMOUNT;
    uint256 immutable public RESERVED_CONTRIBUTORS_AMOUNT;
    uint256 immutable public MAX_FUTURE_LANDS;
    uint256 constant public MAX_MINT_PER_BLOCK = 150;
    
    // structs
    struct LandAmount {
        uint256 alpha;
        uint256 beta;
        uint256 publicSale;
        uint256 future;
    }
    struct ContributorAmount {
        address contributor;
        uint256 amount;
    }
    
    struct Metadata {
        bytes32 metadataHash;
        bytes32 shuffledArrayHash;
        uint256 startIndex;
        uint256 endIndex;
    }
    

![metadata](https://storage.googleapis.com/papyrus_images/142498e63d9cfe25212a2324b86c972bca2a357f0c8c067531fffdb63589db5f.png)

metadata

    struct ContractAddresses {
        address alphaContract;
        address betaContract;
        address tokenContract;
    }
    
    // modifiers
    。。。。。。。。。。
    
    // events
    。。。。。。。。。。
    
    constructor(string memory name, string memory symbol,
        ContractAddresses memory addresses,
        LandAmount memory amount,
        ContributorAmount[] memory _contributors,
        address _vrfCoordinator, address _linkTokenAddress,
        bytes32 _vrfKeyHash, uint256 _vrfFee,
        address _operator
    ) ERC721(name, symbol) VRFConsumerBase(_vrfCoordinator, _linkTokenAddress) {
        //无聊猿和变异猿的地址，持有这个的能够mint（具体的业务忘记了）
        alphaContract = addresses.alphaContract;
        betaContract = addresses.betaContract;
        tokenContract = addresses.tokenContract;
    
        MAX_ALPHA_NFT_AMOUNT = amount.alpha;
        MAX_BETA_NFT_AMOUNT = amount.beta;
        MAX_PUBLIC_SALE_AMOUNT = amount.publicSale;
        MAX_FUTURE_LANDS = amount.future;
    
        betaNftIdCurrent = amount.alpha; //beta starts after alpha
        mintIndexPublicSaleAndContributors = amount.alpha + amount.beta; //public sale starts after beta
    
        //贡献者mint数量
        uint256 tempSum;
        for(uint256 i; i<_contributors.length; ++i){
            contributors[_contributors[i].contributor] = _contributors[i].amount;
            tempSum += _contributors[i].amount;
        }
        RESERVED_CONTRIBUTORS_AMOUNT = tempSum;
        //总数量
        MAX_LANDS = amount.alpha + amount.beta + amount.publicSale + RESERVED_CONTRIBUTORS_AMOUNT;
        //以后还可以mint的数量
        MAX_LANDS_WITH_FUTURE = MAX_LANDS + amount.future;
        futureLandsNftIdCurrent = MAX_LANDS; //future starts after public sale
        //用于随机数请求
        keyHash  = _vrfKeyHash;
        fee = _vrfFee;
        //操作者地址
        operator = _operator;
    }
    
    //一些set函数
    。。。。。。。。。。
    
    // Public Sale Methods
    //公售
    function startPublicSale(
        uint256 _publicSalePriceLoweringDuration, 
        uint256 _publicSaleStartPrice, 
        uint256 _publicSaleEndingPrice,
        uint256 _maxMintPerTx,
        uint256 _maxMintPerAddress,
        bool _isKycCheckRequired
    ) external onlyOperator {
        require(!publicSaleActive, "Public sale has already begun");
        //不用管，荷兰拍取消了
        publicSalePriceLoweringDuration = _publicSalePriceLoweringDuration;
        publicSaleStartPrice = _publicSaleStartPrice;
        publicSaleEndingPrice = _publicSaleEndingPrice;
        publicSaleStartTime = block.timestamp;
        publicSaleActive = true;
        //每次，每个地址的mint限制
        maxMintPerTx = _maxMintPerTx;
        maxMintPerAddress = _maxMintPerAddress;
    
        isKycCheckRequired = _isKycCheckRequired;
    
        emit LandPublicSaleStart(publicSalePriceLoweringDuration, publicSaleStartTime);
    }
    

![startPublicSale function params](https://storage.googleapis.com/papyrus_images/d20d55308bf2acc7c38cd99f3c272293d74b2497cd1375e95216b6d757c57ae6.png)

startPublicSale function params

    
    //停止公售
    function stopPublicSale() external onlyOperator whenPublicSaleActive {
        emit LandPublicSaleStop(getMintPrice(), getElapsedSaleTime());
        publicSaleActive = false;
    }
    //本来是荷兰拍的，取消了，改成固定价格
    function getElapsedSaleTime() private view returns (uint256) {
        return publicSaleStartTime > 0 ? block.timestamp - publicSaleStartTime : 0;
    }
    
    function getMintPrice() public view whenPublicSaleActive returns (uint256) {
        uint256 elapsed = getElapsedSaleTime();
        uint256 price;
    
        if(elapsed < publicSalePriceLoweringDuration) {
            // Linear decreasing function
            price =
                publicSaleStartPrice -
                    ( ( publicSaleStartPrice - publicSaleEndingPrice ) * elapsed ) / publicSalePriceLoweringDuration ;
        } else {
            price = publicSaleEndingPrice;
        }
    
        return price;
    }
    
    //mint land
    function mintLands(uint256 numLands, bytes32[] calldata merkleProof) external whenPublicSaleActive nonReentrant {
        //最少一块土地
        require(numLands > 0, "Must mint at least one beta");
        //mint的土地有没有超过最大限制
        require(currentNumLandsMintedPublicSale + numLands <= MAX_PUBLIC_SALE_AMOUNT, "Minting would exceed max supply");
        //每次mint限制
        require(numLands <= maxMintPerTx, "numLands should not exceed maxMintPerTx");
        //每个人的mint限制
        require(numLands + mintedPerAddress[msg.sender] <= maxMintPerAddress, "sender address cannot mint more than maxMintPerAddress lands");
        //是否需要kyc验证，贡献者需要，公售的不要
        if(isKycCheckRequired) {
            require(MerkleProof.verify(merkleProof, kycMerkleRoot, keccak256(abi.encodePacked(msg.sender))), "Sender address is not in KYC allowlist");
        } else {
            //不允许合约等间接调用，老老实实正常操作
            require(msg.sender == tx.origin, "Minting from smart contracts is disallowed");
        }
     
        //获得土地价格
        uint256 mintPrice = getMintPrice();
        //交钱
        IERC20(tokenContract).safeTransferFrom(msg.sender, address(this), mintPrice * numLands);
        //记录当前的土地数量
        currentNumLandsMintedPublicSale += numLands;
        //记录每个用户的的土地数量
        mintedPerAddress[msg.sender] += numLands;
        emit PublicSaleMint(msg.sender, numLands, mintPrice);
        //mint land
        mintLandsCommon(numLands, msg.sender);
    }
    //调用erc721 mint
    function mintLandsCommon(uint256 numLands, address recipient) private {
        for (uint256 i; i < numLands; ++i) {
            _safeMint(recipient, mintIndexPublicSaleAndContributors++);
        }
    }
    //赚钱喽，取钱
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        if(balance > 0){
            Address.sendValue(payable(owner()), balance);
        }
    
        balance = IERC20(tokenContract).balanceOf(address(this));
        if(balance > 0){
            IERC20(tokenContract).safeTransfer(owner(), balance);
        }
    }
    
    // Alpha/Beta Claim Methods
    //无聊猿和变异猿持有者是否可以再mint
    function flipClaimableState() external onlyOperator {
        claimableActive = !claimableActive;
        emit ClaimableStateChanged(claimableActive);
    }
    
    //无聊猿和变异猿持有者再mint
    function nftOwnerClaimLand(uint256[] calldata alphaTokenIds, uint256[] calldata betaTokenIds) external whenClaimableActive {
        require(alphaTokenIds.length > 0 || betaTokenIds.length > 0, "Should claim at least one land");
        require(alphaTokenIds.length + betaTokenIds.length <= MAX_MINT_PER_BLOCK, "Input length should be <= MAX_MINT_PER_BLOCK");
        //无聊猿持有者mint
        alphaClaimLand(alphaTokenIds);
        //变异猿持有者mint
        betaClaimLand(betaTokenIds);
    }
    
    function alphaClaimLand(uint256[] calldata alphaTokenIds) private {
        for(uint256 i; i < alphaTokenIds.length; ++i){
            uint256 alphaTokenId = alphaTokenIds[i];
            require(!alphaClaimed[alphaTokenId], "ALPHA NFT already claimed");
            require(ERC721(alphaContract).ownerOf(alphaTokenId) == msg.sender, "Must own all of the alpha defined by alphaTokenIds");
            
            alphaClaimLandByTokenId(alphaTokenId);    
        }
    }
    
    function alphaClaimLandByTokenId(uint256 alphaTokenId) private {
        alphaClaimed[alphaTokenId] = true;
        ++alphaClaimedAmount;        
        _safeMint(msg.sender, alphaTokenId);
    }
    
    
    // Contributors Claim Methods
    //开始贡献者mint
    function startContributorsClaimPeriod() onlyOperator external {
        require(!contributorsClaimActive, "Contributors claim is already active");
        contributorsClaimActive = true;
        emit ContributorsClaimStart(block.timestamp);
    }
    //结束贡献者mint
    function stopContributorsClaimPeriod() onlyOperator external whenContributorsClaimActive {
        contributorsClaimActive = false;
        emit ContributorsClaimStop(block.timestamp);
    }
    
    function contributorsClaimLand(uint256 amount, address recipient) external onlyContributors(msg.sender) whenContributorsClaimActive {
        require(amount > 0, "Must mint at least one land");
        require(amount <= MAX_MINT_PER_BLOCK, "amount should not exceed MAX_MINT_PER_BLOCK");
        require(amount <= contributors[msg.sender], "Contributor cannot claim other lands");
    
        contributors[msg.sender] -= amount;
        mintLandsCommon(amount, recipient);
    }
    //应该是把没卖完的土地mint掉，但是不可能出现这种事情的吧。。。。留个后手总归好的
    function claimUnclaimedAndUnsoldLands(address recipient) external onlyOwner {
        claimUnclaimedAndUnsoldLandsWithAmount(recipient, MAX_MINT_PER_BLOCK);
    }
    。。。。。。。。。
    
    // metadata
    //用于给别人验证，保证我的土拍是没有作弊的
    function loadLandMetadata(Metadata memory _landMetadata)
        external onlyOperator checkMetadataRange(_landMetadata)
        checkFirstMetadataRange(metadataHashes.length, _landMetadata.startIndex, _landMetadata.endIndex)
    {
        metadataHashes.push(_landMetadata);
    } 
    
    function putLandMetadataAtIndex(uint256 index, Metadata memory _landMetadata)
        external onlyOperator checkMetadataRange(_landMetadata)
        checkFirstMetadataRange(index, _landMetadata.startIndex, _landMetadata.endIndex)
    {
        metadataHashes[index] = _landMetadata;
    }     
    
    // randomness
    //用link vrf生成随机数，这个随机数给公售和贡献者用
    function requestRandomnessForPublicSaleAndContributors() external onlyOperator returns (bytes32 requestId) {
        require(!publicSaleAndContributorsRandomnessRequested, "Public Sale And Contributors Offset already requested");
        publicSaleAndContributorsRandomnessRequested = true;
        requestId = requestRandomnessPrivate();
        isRandomRequestForPublicSaleAndContributors[requestId] = true;
    }
    
    //用link vrf生成随机数，这个随机数给猿持有者用
    function requestRandomnessForOwnerClaim() external onlyOperator returns (bytes32 requestId) {
        require(!ownerClaimRandomnessRequested, "Owner Claim Offset already requested");
        ownerClaimRandomnessRequested = true;
        requestId = requestRandomnessPrivate();
        isRandomRequestForPublicSaleAndContributors[requestId] = false;
    }
    
    function requestRandomnessPrivate() private returns (bytes32 requestId) {
        require(
            LINK.balanceOf(address(this)) >= fee,
            "Not enough LINK"
        );
        return requestRandomness(keyHash, fee);
    }
    //link会把产生的随机数回调这个函数，然后项目方进行业务处理，这里猜测应该是概率不同，我持有nft，那拿到好土地的概率会更大
    function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
        //区分是公售、贡献者还是猿持有者随机数
        if(isRandomRequestForPublicSaleAndContributors[requestId]){
            publicSaleAndContributorsOffset = (randomness % (MAX_PUBLIC_SALE_AMOUNT + RESERVED_CONTRIBUTORS_AMOUNT));
            emit StartingIndexSetPublicSale(publicSaleAndContributorsOffset);
        } else {
            alphaOffset = (randomness % MAX_ALPHA_NFT_AMOUNT);
            betaOffset = (randomness % MAX_BETA_NFT_AMOUNT);
            emit StartingIndexSetAlphaBeta(alphaOffset, betaOffset);
        }
    }

---

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