# MetaFriends智能合约的7天无理由退款功能浅析

By [JCONE](https://paragraph.com/@jcone) · 2022-04-21

---

MetaFriends智能合约的7天无理由退款功能浅析

MetaFriends是新一个以ERC721 A+R 作为宣传点的NFT项目，并在今天开启了Presale Mint（[官网](https://www.metafriendsnft.io/)）。

ERC721R是在ERC721A基础上添加了某个期限内无理由退款功能，允许铸造者退还NFT并获得铸造价格的全额退款（相当于只损失了gas fee），使得项目方在这个期限内无法withdrawn铸造所得资金，从而一定程度上保护了NFT买家的利益，也有助于防止项目在退款期内地板价破发。

让我们阅读MetaFriends智能合约退款的[源代码](https://etherscan.io/address/0xc1ae3fe5f93c2c042fac7bdcdf94fdd673544e31#code)，看看其天无理由退款功能是如何实现，是否有漏洞。

    uint256 public immutable refundPeriod = 7 days;
    uint256 public lastMintTime;
    

首先设定为退款期限`refundPeriod`为7天，不可更改，并定义了一个整型变量名为`lastMintTime`；

    address public refundAddress;
    

这里设定了一个退货地址`refundAddress`，用于接收退回的NFT，MetaFriends设置了与合约部署者同一个钱包地址为退货地址；

    mapping(uint256 => bool) public hasRefunded;
    

为每一个`tokenId`映射了一个`hasRefunded`的bool变量，记录某个tokenId是否已经退过款，两种状态为true或false；

    mapping(uint256=>uint256) private timestampID;
    

为每一个`tokenId`映射了一个`timestampID`时间值，

    function _setMintTime(uint256 amount) internal{
        if (amount == 5){
            timestampID[totalSupply()-1] = block.timestamp;
            timestampID[totalSupply()-2] = block.timestamp;
            timestampID[totalSupply()-3] = block.timestamp;
            timestampID[totalSupply()-4] = block.timestamp;
            timestampID[totalSupply()-5] = block.timestamp;
        }
        if (amount == 4){
            timestampID[totalSupply()-1] = block.timestamp;
            timestampID[totalSupply()-2] = block.timestamp;
            timestampID[totalSupply()-3] = block.timestamp;
            timestampID[totalSupply()-4] = block.timestamp;
    
        }
        if (amount == 3){
            timestampID[totalSupply()-1] = block.timestamp;
            timestampID[totalSupply()-2] = block.timestamp;
            timestampID[totalSupply()-3] = block.timestamp;
        }
        if (amount == 2){
            timestampID[totalSupply()-1] = block.timestamp;
            timestampID[totalSupply()-2] = block.timestamp;
        }else{
            timestampID[totalSupply()-1] = block.timestamp;
        }  
    
        lastMintTime = block.timestamp;
    }
    

因为MetaFriends设定的最大单笔交易mint数量`maxMintPerTx`为5，所以这里区分了铸造1-5个不同数量的5种交易情况，把每个`tokenId`的`timestampID`设定为当前交易的时间戳，并定义当前交易时间戳为`lastMintTime`；

    function refund(uint256 tokenId) external {
        require(msg.sender == ownerOf(tokenId), "Not token owner");
        require(!hasRefunded[tokenId], "Already refunded");
        require(block.timestamp - timestampID[tokenId] < refundPeriod, "refund time expire");
        hasRefunded[tokenId] = true;
        transferFrom(msg.sender, refundAddress, tokenId);
        Address.sendValue(payable(msg.sender), PRICE);
    }
    

这里定义了退款功能函数，先设定三个退款的条件，一是必须为tokenId的owner（`msg.sender == ownerOf(tokenId)`），二是此tokenId之前没有退过款（`！hasRefunded[tokenId]`），三是当前时间戳减去tokenId铸造时间戳必须小于`refundPeriod`7天（`block.timestamp - timestampID[tokenId] < refundPeriod`），如果三个条件都满足，则将`hasRefunded`设定为true，然后转移退款者钱包中的tokenId到退货地址`refundAddress`，并从智能合约地址里把铸造费用发送给退款者，实现了NFT的退货退款。

这里`!hasRefunded`的require非常重要，部分早期ERC721R代码因为缺乏这一个约束条件，产生了一个项目方可以提前攫取资金的漏洞，通过`refundAddress`拿到退回来的NFT，继续在智能合约中进行循环退款`refund`操作，直至完全耗尽合约的资金，从而导致其他tokenId的`refund`无法实现；有了`!hasRefunded`的require之后，MetaFriends的合约并无此问题；

另外一个早期ERC721R的问题是价格归零漏洞，项目方在mint结束后，通过将`PRICE`设置为0，致使退款者将NFT退回之后无法获得任何退款，导致财货两空，因此用户需要对退款行为极为小心；MetaFriends的合约中并没有修改`PRICE`的功能，没有此问题；

    function withdrawAll() external onlyOwner {
        require(block.timestamp - lastMintTime > refundPeriod, "Refund period not over");
        uint256 balance = address(this).balance;
        require(balance > 0);
        _widthdraw(0xc2AE244F0c0025864ac1Fd87Af4c8202bcD9a78F, balance.mul(70).div(100));
        _widthdraw(0x902e3BF232Da0cBf91232a15460171f613ADad8e, address(this).balance);
    }
    

最后一个是项目方`withdrawAll`资金的功能函数，增加了一个约束条件，即操作时间戳减去最后一个mint时间戳`lastMintTime`必须大于`refundPeriod`7天，因为`refundPeriod`又是不可更改的常量，因此保证了退款期内项目方无法取走合约中的资金；

总结：通过增加约束条件限制循环退款，未设置mint价格修改功能，可以说MetaFriends合约是真正在代码端实现了无需信任（trustless）的7天无理由退款。

JCONE.eth

2022/4/21

---

*Originally published on [JCONE](https://paragraph.com/@jcone/metafriends-7)*
