# ERC721R协议 **Published by:** [BlockInvisibleMan](https://paragraph.com/@blockinvisibleman/) **Published on:** 2022-04-23 **URL:** https://paragraph.com/@blockinvisibleman/erc721r ## Content 短语宇宙就是一座黑暗森林,每个文明都是带枪的猎人,像幽灵般潜行于林间,轻轻拨开挡路的树枝,竭力不让脚步发出一点儿声音,连呼吸都小心翼翼他必须小心,因为林中到处都有与他一样潜行的猎人。——《黑暗森林》 Dark Forest(黑暗森林)这个概念来源于《三体》,Dan和 Georgios 从范式中推广的加密术语,其中用来描述以太坊的环境。以太坊是一个高度对抗的环境,链本身就是一个战场,同样包括内存池。在我看来加密货币交易亦是如此,充斥着混沌、无秩序、野蛮、掠夺。这就是区块链的世界,是每个web3.0(区块链)探索者必经之路。欢迎来到黑暗森林!ERC721R介绍ERC721R:为NFT创造者带来更多的责任感 ERC721R主要作用区别于ERC721、ERC1155以及ERC875的作用主要为 NFT 智能合约添加了无需信任的退款,允许铸币者在给定的退款期内返还以成本铸造的 NFT。 优势买家30天内可以退款,只损失手续费退回的代表金额不会低于mint时的出售价格增加创作者的责任感减少买家购买NFT的风险劣势权限过大,refund函数中管理员权限过大,当用户进行退款时,管理员将得到退款的NFT,refund中并没有对管理员做些权限限制,管理员也可以操作退款NFT,耗尽合约的资金合约漏洞,买家没有归还NFT的时间段内,管理员可以铸造NFT,然后像上一步的描述中那样,耗尽合约资金ERC721R实现https://github.com/exo-digital-labs/ERC721R/blob/main/contracts/ERC721RExample.sol状态变量//ERC721Reciver contract ERC721RExample is ERC721A, Ownable { // 最大铸造数量 uint256 public constant maxMintSupply = 8000; // 保护的最低价格 uint256 public constant mintPrice = 0.1 ether; // NFT退回时间 uint256 public constant refundPeriod = 45 days; // 公开销售活跃状态 bool public publicSaleActive; // 预售活跃状态 bool public presaleActive; // 退回结束时间 uint256 public refundEndTime; // 退回的管理者地址 address public refundAddress; // 用户最大铸造的数量 uint256 public constant maxUserMintAmount = 5; // 默克尔树根 bytes32 public merkleRoot; // 已经退回的NFT,通过NFT的id查询 mapping(uint256 => bool) public hasRefunded; // users can search if the NFT has been refunded // 是否是所有者铸造的NFT mapping(uint256 => bool) public isOwnerMint; // if the NFT was freely minted by owner // NFT的基本链接 string private baseURI; // 初始化NFT名称和标识 constructor() ERC721A("ERC721RExample", "ERC721R") { // 退回地址为部署者,也就是管理员 refundAddress = msg.sender; toggleRefundCountdown(); } 预售NFT预售NFT的函数是preSafeMint,可以理解为早期的空投、私募和一级市场的买家获得的资格// 铸造NFT // @param `quantity` 铸造数量 // @param `proof` 叶子到树根的分支上的兄弟姐妹哈希值 function preSaleMint(uint256 quantity, bytes32[] calldata proof) 确保预售已经开始require(presaleActive, "Presale is not active"); 确保当前买家的余额 == NFT数量 * 铸造价格require(msg.value == quantity * mintPrice, "Value"); 确定买家在预售白名单中,通过MerkleProof进行判断require( _isAllowlisted(msg.sender, proof, merkleRoot), "Not on allow list" ); 买家可铸造NFT的数量 + NFT数量 <= 用户最大的铸造数量require( _numberMinted(msg.sender) + quantity <= maxUserMintAmount, "Max amount" ); 供应的铸造数量 + 铸造的数量 <= 最大铸造数量require(_totalMinted() + quantity <= maxMintSupply, "Max mint supply"); 安全铸造_safeMint(msg.sender, quantity); 公开销售NFT公开销售NFT的函数是publicSaleMint,和preSaleMint逻辑一样 // 公开销售NFT // @param `quantity` 铸造数量 function publicSaleMint(uint256 quantity) external payable 确保公开销售已经开始require(publicSaleActive, "Public sale is not active"); 确保当前买家的余额 == NFT数量 * 铸造价格require(msg.value >= quantity * mintPrice, "Not enough eth sent"); 买家可铸造NFT的数量 + NFT数量 <= 用户最大的铸造数量require( _numberMinted(msg.sender) + quantity <= maxUserMintAmount, "Over mint limit" ); 供应的铸造数量 + 铸造的数量 <= 最大铸造数量require( _totalMinted() + quantity <= maxMintSupply, "Max mint supply reached" ); 安全铸造_safeMint(msg.sender, quantity); 所有者铸造NFT所有者铸造NFT的函数是ownerMint,只允许所有者调用function ownerMint(uint256 quantity) external onlyOwner 供应的铸造数量 + 铸造的数量 <= 最大铸造数量require( _totalMinted() + quantity <= maxMintSupply, "Max mint supply reached" ); 安全铸造_safeMint(msg.sender, quantity); 将所有者铸造的所有NFT编号映射在isOwnerMintfor (uint256 i = _currentIndex - quantity; i < _currentIndex; i++) { isOwnerMint[i] = true; } 退回NFT退回NFT的函数是refund,由买家外部调用// @param `tokenIds` 退回的NFT编号数组 function refund(uint256[] calldata tokenIds) external 退回NFT的时间是否过期require(isRefundGuaranteeActive(), "Refund expired"); 确保NFT已经售出require(msg.sender == ownerOf(tokenId), "Not token owner"); 确保NFT没有重复退回require(!hasRefunded[tokenId], "Already refunded"); 确保NFT不属于所有者铸造require(!isOwnerMint[tokenId], "Freely minted NFTs cannot be refunded"); 记录退回NFT的状态hasRefunded[tokenId] = true; 退回金额 = NFT数量 * 铸造时的价格uint256 refundAmount = tokenIds.length * mintPrice; 调用transferFrom进行NFT转让transferFrom(msg.sender, refundAddress, tokenId); 退回NFT的资金给买家Address.sendValue(payable(msg.sender), refundAmount); 提取资金提前资金的函数是withdraw,仅限管理员操作function withdraw() external onlyOwner 确保提取资金时间在退款结束后require(block.timestamp > refundEndTime, "Refund period not over"); 获取合约余额uint256 balance = address(this).balance; 提现合约资金Address.sendValue(payable(owner()), balance); 非核心的函数// 判断是否退回时间内 function isRefundGuaranteeActive() public view returns (bool) { return (block.timestamp <= refundEndTime); } // 返回NFT的基本链接 function _baseURI() internal view override returns (string memory) { return baseURI; } // 设置NFT退回的地址,只允许管理员调用 function setRefundAddress(address _refundAddress) external onlyOwner { refundAddress = _refundAddress; } // 设置Merkle根,只允许管理员调用 function setMerkleRoot(bytes32 _root) external onlyOwner { merkleRoot = _root; } // 设置基本链接,只允许管理员调用 function setBaseURI(string memory uri) external onlyOwner { baseURI = uri; } // 设置退回时间,只允许管理员调用 function toggleRefundCountdown() public onlyOwner { refundEndTime = block.timestamp + refundPeriod; } // 切换预售状态,只允许管理员调用 function togglePresaleStatus() external onlyOwner { presaleActive = !presaleActive; } // 切换公开销售状态,只允许管理员调用 function togglePublicSaleStatus() external onlyOwner { publicSaleActive = !publicSaleActive; } // 叶子节点 function _leaf(address _account) internal pure returns (bytes32){ return keccak256(abi.encodePacked(_account)); } // 调用MerkleProof, 判断是否满足MerkleProot function _isAllowlisted( address _account, bytes32[] calldata _proof, bytes32 _root ) internal pure returns (bool) { return MerkleProof.verify(_proof, _root, _leaf(_account)); } ## Publication Information - [BlockInvisibleMan](https://paragraph.com/@blockinvisibleman/): Publication homepage - [All Posts](https://paragraph.com/@blockinvisibleman/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@blockinvisibleman): Subscribe to updates