erc721中safeMint与mint的区别

目标:向大家解释一下erc721中safeMint与mint的区别(safeTransferFrom与transferFrom同理)
章节流程:

  1. 理论为先
  2. 猜测预期
  3. 验证猜测
  4. 结论

一、理论为先

首先,我们看一下示例代码(文件ERC721Upgradeable.solIERC721ReceiverUpgradeable.sol)
示例代码地址

  1. 文件ERC721Upgradeable.sol中的关键内容:
    /**
     * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
     * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
     */
    function _safeMint(
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, _data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
     *
     * Requirements:
     *
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     *
     * Emits a {Transfer} event.
     */
    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");

        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
    }

    function _checkOnERC721Received(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) private returns (bool) {
        if (to.isContract()) {
            try IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
                return retval == IERC721ReceiverUpgradeable.onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ERC721: transfer to non ERC721Receiver implementer");
                } else {
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }
  1. 文件IERC721ReceiverUpgradeable.sol中的内容:
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @title ERC721 token receiver interface
 * @dev Interface for any contract that wants to support safeTransfers
 * from ERC721 asset contracts.
 */
interface IERC721ReceiverUpgradeable {
    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
     *
     * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

二、猜测预期

通过上述示例源码我们可以发现:其实方法safeTransferFrom中额外校验了一下IERC721ReceiverUpgradeable(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval)的执行结果是否通过(注:通过则返回正确的方法签名-IERC721.onERC721Received.selector),通过交易成功,如果不通过交易则回滚。

那接下来,我们提出一个猜测:“接受方为合约账户时,可以通过重写方法onERC721Received进行交易的安全校验”。

三、验证猜测

我们来准备两个简单的合约,一个为自定义的721合约MyToken721UUPSV1.sol,另一个为实现onERC721Received方法的自定义接收方合约MyToken721UUPSV1_Holder.sol

接收方合约验证来自721合约mint方法传递的参数data是否为预期数据,是则交易通过,不是则交易不通过使其回滚,达到安全的效果。

具体代码如下:

  1. 文件MyToken721UUPSV1.sol中的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract MyToken721UUPSV1 is ERC721Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() initializer {}

    function initialize() initializer public {
        __ERC721_init("MyToken", "MTK");
        __Ownable_init();
        __UUPSUpgradeable_init();
    }

    // 传递data为“bsn”
    // 注:to需要为合约账户
    function safeMint(address to, uint256 tokenId) public onlyOwner {
        bytes memory data = new bytes(3);
        data[0]="b";
        data[1]="s";
        data[2]="n";
        _safeMint(to, tokenId,data);
    }

    // 传递data为“zxl”
    // 注:to需要为合约账户
    function safeMintZxl(address to, uint256 tokenId) public onlyOwner {
        bytes memory data = new bytes(3);
        data[0]="z";
        data[1]="x";
        data[2]="l";
        _safeMint(to, tokenId,data);
    }

    function _authorizeUpgrade(address newImplementation)
        internal
        onlyOwner
        override
    {}

    function GetInitializeData() public pure returns(bytes memory){
        return abi.encodeWithSignature("initialize()");
    }

    function myName() public view virtual returns (string memory){
        return "zxlv1";
    }
}
  1. 文件MyToken721UUPSV1_Holder .sol中的内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract MyToken721UUPSV1_Holder is OwnableUpgradeable
, IERC721Receiver 

{
    event ERC721Received( address operator,
        address from,
        uint256 tokenId,
        bytes data);
    // 用于接收合约发来的数据
     function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) public override returns (bytes4) {
        emit ERC721Received(operator,from,tokenId,data);
        //safeMint 传递的参数数据
        bytes memory safedata = new bytes(3);
        safedata[0]="b";
        safedata[1]="s";
        safedata[2]="n";
        // 如果为safeMint则返回正确的结果,否则返回错误的结果。
        if (equal(safedata,data)) {
            return this.onERC721Received.selector;
        } else {
            return this.myName.selector;
        }
    }
    function myName() public view virtual returns (string memory){
        return "MyToken721UUPSV1_Holder";
    }

    function equal( bytes memory self_rep, bytes memory other_rep) internal pure returns(bool){
        if(self_rep.length != other_rep.length){
            return false;
        }
        uint selfLen = self_rep.length;
        for(uint i=0;i<selfLen;i++){
            if(self_rep[i] != other_rep[i]) return false;
        }
        return true;           
    }
}

使用工具MetaMask和Remix部署合约至Rinkeby 测试网络

接下来我们开始测试,分别调用safeMint方法和safeMintZxl方法

  • 执行721合约的safeMint方法,执行成功,详情如下:
    注:传递数据为"bsn"

交易输出截图:
交易hash:https://rinkeby.etherscan.io/tx/0x4f6fdacb3a2221103da71126820d5309ae19341d787a77e9cfede26479eb5c56

image.png

结果验证: 调用方法ownerof验证1是否mint至账户0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B,截图如下:

image.png

在etherscan中查看输出日志: 我在合约里打印了下传递过来的参数

image.png

  • 执行721合约的safeMintZxl方法,执行失败,详情如下:
    注:传递数据"zxl"
    image.png

我们发现已经提示交易异常,如果执意执行发送交易,结果如下:


image.png

也可以多余的验证下2没有mint至账户0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B,调用ownerOf返回ERC721: owner query for nonexistent token,调用balanceOf返回1,截图如下:

image.png

符合预期猜测!!!

四、结论

当接收方为合约账户时,可以使用safeXXX方法(safeMint或者safeTransferFrom)进行交易的安全校验。如果是普通账户的话,两者的执行没有区别。

顺便提一下erc1155也是同样的道理。

以上为个人的观点,欢迎大家一块交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容