erc721中safeMint與mint的區別

BSN研習社發表於2022-03-18

id:BSN_2021


公眾號:BSN研習社


以下文章來源於紅棗科技張雪良


目標:向大家解釋一下erc721中safeMint與mint的區別(safeTransferFrom與transferFrom同理)


章節流程:

    1.理論為先


    2.猜測預期


    3.驗證猜測


    4.結論


一、理論為先


首先,我們看一下示例程式碼(檔案ERC721Upgradeable.sol和IERC721ReceiverUpgradeable.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;
    }
}


2.檔案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";
}
}


 2.檔案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 測試網路:


  • holder:0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B


  • 721:0x376bAfBE6619233b8E4536f2f7CAb611d35BFb79


  • proxy:0x4955db0b2E5C437A5C9e431118967C3699e0Dc43


接下來我們開始測試,分別呼叫 safeMint方法和 safeMintZxl方法


  • 執行721合約的safeMint方法,執行成功,詳情如下:


(注:傳遞資料為"bsn")


交易輸出截圖:


交易hash:


/0x4f6fdacb3a2221103da71126820d5309ae19341d787a77e9cfede26479eb5c56



結果驗證:呼叫方法 ownerof驗證 1是否mint至賬戶 0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B,截圖如下:



在etherscan中檢視輸出日誌: 我在合約裡列印了下傳遞過來的引數



  • 執行721合約的safeMintZxl方法,執行失敗,詳情如下:


(注:傳遞資料"zxl")



我們發現已經提示交易異常,如果執意執行傳送交易,結果如下:



也可以多餘的驗證下 2沒有mint至賬戶 0xe9E8F76524aE41C93Dd2066dFEd3B41fbf21f59B,呼叫 ownerOf返回 ERC721: owner query for nonexistent token,呼叫 balanceOf返回 1,截圖如下:



符合預期猜測!!!


四、結論


當接收方為合約賬戶時,可以使用safeXXX方法(safeMint或者safeTransferFrom)進行交易的安全校驗。如果是普通賬戶的話,兩者的執行沒有區別。


順便提一下erc1155也是同樣的道理。


以上為個人的觀點,歡迎大家一塊交流。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70012206/viewspace-2872402/,如需轉載,請註明出處,否則將追究法律責任。

相關文章