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也是同样的道理。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容