Cover photo

ERC721R协议

短语

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

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编号映射在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));
    }