# ERC721R协议

By [BlockInvisibleMan](https://paragraph.com/@blockinvisibleman) · 2022-04-23

---

> ### 短语

宇宙就是一座黑暗森林，每个文明都是带枪的猎人，像幽灵般潜行于林间，轻轻拨开挡路的树枝，竭力不让脚步发出一点儿声音，连呼吸都小心翼翼他必须小心，因为林中到处都有与他一样潜行的猎人。——《黑暗森林》

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](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编号映射在isOwnerMint
    
        for (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))；
        }

---

*Originally published on [BlockInvisibleMan](https://paragraph.com/@blockinvisibleman/erc721r)*
