# APE土拍代码分析 **Published by:** [point](https://paragraph.com/@point/) **Published on:** 2022-05-06 **URL:** https://paragraph.com/@point/ape ## Content 纯属个人收集整理资料+揣测理解,我也不是NFT玩家,也没这个资金实力玩NFT。所以,理解上没有专业人士准确。业务梳理固定305APE价格售卖售卖分为kyc pre-mint(Contributor才行,应该就是白名单吧)、公售(大家抢)、持有无聊猿和变异猿的人可以再mint5.1买的是盲盒,5.2拆盒operator操作记录operator也是操作了这些事情,业务理解应该没太大问题代码注释删掉了一些业务无关、重复、无需理解的代码 代码还算简单,只有通过随机数将土地和nft721的metadata关联起来有点疑问 通过链上操作可以知道tokenurl是在土拍前就设置了,并且再也没有改过,所以不是其他项目那样卖完后通过改url拆盒 项目方也上传了metadatahashes,所以所有nft metadata是事先就准备好了的 链上的随机数并没有参与链上业务的处理,比如通过随机数+tokenid获取metadata url 既然链上的方法都没有使用,那么这个关联操作应该是放在了链下, api.otherside.xyz/lands/27893 访问这个地址后,通过他们未开源的算法,将27893+随机数指向了metaurl。但是这样是不是就能够通过修改算法,改变用户的nft了。。。。。。。这点没有想明白,如果有好心人愿意指导下我,可以zn978740431@gmail.com给我发邮件。 网上找到两个还算靠谱的实现文章,看起来都是在链上完成这个操作的,搞不明白ape是怎么想的。因为很多nft项目方在这上面做手脚,还是很想理解透这个事情。 https://vocus.cc/article/6212f73ffd89780001687585 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; urladdress 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//每次最大可以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; } metadatastruct 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 //停止公售 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); } } ## Publication Information - [point](https://paragraph.com/@point/): Publication homepage - [All Posts](https://paragraph.com/@point/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@point): Subscribe to updates