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

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

MetaFriends是新一个以ERC721 A+R 作为宣传点的NFT项目,并在今天开启了Presale Mint(官网)。

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

让我们阅读MetaFriends智能合约退款的源代码,看看其天无理由退款功能是如何实现,是否有漏洞。

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种交易情况,把每个tokenIdtimestampID设定为当前交易的时间戳,并定义当前交易时间戳为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铸造时间戳必须小于refundPeriod7天(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必须大于refundPeriod7天,因为refundPeriod又是不可更改的常量,因此保证了退款期内项目方无法取走合约中的资金;

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

JCONE.eth

2022/4/21