# ERC721

By [startha](https://paragraph.com/@starha) · 2023-11-03

---

[EIP与ERC](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#eip%E4%B8%8Eerc)
------------------------------------------------------------------------------------------------------

这里有一点需要理解，本节标题是`erc721`，这里又提到了`EIP721`，这两个是什么关系呢？

`EIP`全称`Ethereum Improvement Proposals`（以太坊改进建议），是以太坊开发者社区提出的改进建议，是一系列以编号排定的文件，类似互联网上IEFT的RFC。

`EIP`可以是`Ethereum`生态中任意领域的改进，比如新特性、ERC、协议改进、编程工具等等。

`ERC`全称`Ethereum Request For Comment`（以太坊意见征询稿），用以记录以太坊上应用级的各种开发标准和协议。如典型的Token标准（`ERC20`，`ERC721`）、名字注册（`ERC26`，`ERC13`），URI规范（`ERC67`），Library/Package格式（`EIP82`），钱包格式（`EIP75`，`EIP85`）。

ERC协议标准是影响以太坊发展的重要因素，像`ERC20`、`ERC223`、`ERC721`、`ERC777`等，都对以太坊生态产生了很大影响。

所以最终结论：`EIP`包含`ERC`。

**在这一节学习完成后，才能明白为什么上来讲**`ERC165`而不是`ERC721`。

[ERC165](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#erc165)
--------------------------------------------------------------------------------------------

通过[ERC165标准](https://eips.ethereum.org/EIPS/eip-165)，智能合约可以声明它支持的接口，供其他合约检查。简单的说，ERC165就是检查一个智能合约是不是支持了`ERC721`，`ERC1155`的接口。

`IERC165`接口合约只声明了一个`supportsInterface`函数，输入要查询的`interfaceId`接口id，若合约实现了该接口id，则返回`true`：

    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    /**
     * @dev ERC165标准接口, 详见
     * https://eips.ethereum.org/EIPS/eip-165[EIP].
     *
     * 合约可以声明支持的接口，供其他合约检查
     *
     */
    interface IERC165 {
        /**
         * @dev 如果合约实现了查询的`interfaceId`，则返回true
         * 规则详见：https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
         *
         */
        function supportsInterface(bytes4 interfaceId) external view returns (bool);
    }
    

我们可以看下`ERC721`是如何实现`supportsInterface()`函数的：

        //实现IERC165接口supportsInterface
        function supportsInterface(bytes4 interfaceId) external pure override returns(bool) {
            return 
            interfaceId == type(IERC721).interfaceId || 
            interfaceId == type(IERC165).interfaceId || 
            interfaceId == type(IERC721Metadata).interfaceId;
        }
    

当查询的是`IERC721`或`IERC165`的接口id时，返回`true`；反之返回`false`。

[IERC721](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#ierc721)
----------------------------------------------------------------------------------------------

`IERC721`是`ERC721`标准的合约接口，规定了`ERC721`要实现的基本函数。它利用`tokenId`来表示特定的非同质化代币，授权和转账都要明确`tokenId`；而`ERC20`只需要明确转账的数额即可。

    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    import "./IERC165.sol";
    
    /**
     * @dev ERC721标准接口.
     */
    interface IERC721 is IERC165 {
        event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
        event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
        event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
        function balanceOf(address owner) external view returns (uint256 balance);
    
        function ownerOf(uint256 tokenId) external view returns (address owner);
    
        function safeTransferFrom(address from,address to,uint256 tokenId,bytes calldata data) external;
    
        function safeTransferFrom(address from,address to,uint256 tokenId) external;
    
        function transferFrom(address from,address to,uint256 tokenId) external;
    
        function approve(address to, uint256 tokenId) external;
    
        function setApprovalForAll(address operator, bool _approved) external;
    
        function getApproved(uint256 tokenId) external view returns (address operator);
    
        function isApprovedForAll(address owner, address operator) external view returns (bool);
    }
    

### [IERC721事件](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#ierc721%E4%BA%8B%E4%BB%B6)

`IERC721`包含3个事件，其中`Transfer`和`Approval`事件在`ERC20`中也有。

*   `Transfer`事件：在转账时被释放，记录代币的发送地址`from`，接收地址`to`和`tokenid`。
    
*   `Approval`事件：在授权时释放，记录授权地址`owner`，被授权地址`approval`和`tokenid`。
    
*   `ApprovalForAll`s事件：在批量授权时释放，记录批量授权时发出地址`owner`，被授权地址`operator`和授权与否的`approved`。
    

### [IERC721函数](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#ierc721%E5%87%BD%E6%95%B0)

*   `balanceOf`：返回某地址的NFT持有量`balance`。
    
*   `ownerof`：返回某`tokenId`的主人`owner`。
    
*   `transferFrom`：普通转账，参数为输出地址`from`，接收地址`to`和`tokenId`。
    
*   `safeTransferFrom`：安全转账（如果接收方是合约地址，会要求实现`ERC721Receiver`接口）。参数为转出地址`from`，接收地址`to`和`tokenId`。
    
*   `approve`：授权另一个地址使用你的NFT。参数为被授权地址`approve`和`tokenId`。
    
*   `getApproved`：查询`tokenId`被批准给哪个地址。
    
*   `setApprovalForAll`：将自己持有的该系列NFT批量授权给某个地址`operator`。
    
*   `isApprovedForAll`：查询某地址的NFT是否批量授权给了另一个`operator`。
    
*   `safeTransferFrom`：安全转账的重载函数，参数里面包含了`data`。
    

[IERC721Receiver](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#ierc721receiver)
--------------------------------------------------------------------------------------------------------------

如果一个合约没有实现`ERC721`的相关函数，转入的`NFT`就进入了黑洞，永远转不出来了。为了防止误转账，`ERC721`实现了`safeTransferFrom()`安全转账函数，目标合约必须实现了`IERC721Receiver`接口才能接收`ERC721`代币，不然会`revert`。`IERC721Received()`函数。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    // ERC721接收者接口：合约必须实现这个接口来通过安全转账接收ERC721
    interface IERC721Receiver {
        function onERC721Received(address operator,address from,uint tokenId,bytes calldata data) external returns (bytes4);
    }
    

我们看下`ERC721`利用`_checkOnERC721Received`来确保目标合约实现了`onERC721Received()`函数（返回`onERC721Recevied`的`selector`）：

        // _checkOnERC721Received：函数，用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
        function _checkOnERC721Received(address from,address to,uint tokenId,bytes memory _data) private returns (bool) {
            if (to.isContract()) {
                return IERC721Receiver(to).onERC721Received(msg.sender,from,tokenId,_data) == 
                IERC721Receiver.onERC721Received.selector;
            } else {
                return true;
            }
        }
    

[IERC721Metadata](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#ierc721metadata)
--------------------------------------------------------------------------------------------------------------

`IERC721Metadata`是`ERC721`的扩展接口，实现了3个查询`metadata`元数据的常用函数：

*   `name()`：返回代币名称。
    
*   `symbol()`：返回代币代号。
    
*   `tokenURI()`：通过`tokenId`查询`metadata`的链接`url`，`ERC721`特有的函数。
    

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    interface IERC721Metadata {
        function name() external view returns (string memory);
    
        function symbol() external view returns (string memory);
    
        function tokenURI(uint256 tokenId) external view returns (string memory);
    }
    

[ERC721主合约](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#erc721%E4%B8%BB%E5%90%88%E7%BA%A6)
--------------------------------------------------------------------------------------------------------------------------

`ERC721`主合约实现了`IERC721`，`IERC165`和`IERC721Metadata`定义的所有功能，包含4个状态变量和17个函数。实现都比较简单。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "./IERC165.sol";
    import "./IERC721.sol";
    import "./IERC721Receiver.sol";
    import "./IERC721Metadata.sol";
    import "./Address.sol";
    import "./String.sol";
    
    contract ERC721 is IERC721, IERC721Metadata {
        using Address for address;// 使用Address库，用isContract来判断地址是否为合约
        using Strings for uint256; //使用String库
    
        string public override name; //Token名称
        string public override symbol; //Token代号
        mapping(uint => address) private _owners;// tokenId 到 owner address 的持有人映射
        mapping(address => uint) private _balances;//address到持仓数量的持仓量映射
        mapping(uint => address) private _tokenApprovals; // tokenId 到 授权地址 的授权映射
        mapping(address => mapping(address => bool)) private _operatorApprovals; //  owner地址。到operator地址 的批量授权映射
    
        //构造函数初始化name和symbol
        constructor(string memory name_, string memory symbol_) {
            name = name_;
            symbol = symbol_;
        }
    
        //实现IERC165接口supportsInterface
        function supportsInterface(bytes4 interfaceId) external pure override returns(bool) {
            return 
            interfaceId == type(IERC721).interfaceId || 
            interfaceId == type(IERC165).interfaceId || 
            interfaceId == type(IERC721Metadata).interfaceId;
        }
    
        //实现IERC721balanceOf，利用_balances变量查询owner地址的balance
        function balanceOf(address owner) external view override returns(uint) {
            require(owner != address(0), "owner = zero address");
            return _balances[owner];
        }
    
        //实现IERC721的ownerOf，利用_owners变量查询tokenId的owner
        function ownerOf(uint tokenId) public view override returns(address owner) {
            owner = _owners[tokenId];
            require(owner != address(0), "token doesn't exist");
        }
    
        // 实现IERC721的isApprovedForAll，利用_operatorApprovals变量查询owner地址是否将所持NFT批量授权给了operator地址。
        function isApprovedForAll(address owner, address operator) external view override returns(bool) {
            return _operatorApprovals[owner][operator];
        }
    
        // 实现IERC721的setApprovalForAll，将持有代币全部授权给operator地址。调用_setApprovalForAll函数。
        function setApprovalForAll(address operator, bool approved) external override {
            _operatorApprovals[msg.sender][operator] = approved;
            emit ApprovalForAll(msg.sender, operator, approved);
        }
    
        // 实现IERC721的getApproved，利用_tokenApprovals变量查询tokenId的授权地址。
        function getApproved(uint tokenId) external view override returns (address) {
            require(_owners[tokenId] != address(0), "token doesn't exist");
            return _tokenApprovals[tokenId];
        }
    
        // 授权函数。通过调整_tokenApprovals来，授权 to 地址操作 tokenId，同时释放Approval事件。
        function _approve(address owner,address to,uint tokenId) private {
            _tokenApprovals[tokenId] = to;
            emit Approval(owner, to, tokenId);
        }
    
        // 实现IERC721的approve，将tokenId授权给 to 地址。条件：to不是owner，且msg.sender是owner或授权地址。调用_approve函数。
        function approve(address to, uint tokenId) external override {
            address owner = _owners[tokenId];
            require(
                msg.sender == owner || _operatorApprovals[owner][msg.sender],
                "not owner nor approved for all"
            );
            _approve(owner, to, tokenId);
        }
    
        // 查询 spender地址是否可以使用tokenId（需要是owner或被授权地址）
        function _isApprovedOrOwner(address owner,address spender,uint tokenId) private view returns (bool) {
            return (spender == owner ||
                _tokenApprovals[tokenId] == spender ||
                _operatorApprovals[owner][spender]);
        }
    
        /*
         * 转账函数。通过调整_balances和_owner变量将 tokenId 从 from 转账给 to，同时释放Transfer事件。
         * 条件:
         * 1. tokenId 被 from 拥有
         * 2. to 不是0地址
         */
        function _transfer(address owner,address from,address to,uint tokenId) private {
            require(from == owner, "not owner");
            require(to != address(0), "transfer to the zero address");
            _approve(owner, address(0), tokenId);
            _balances[from] -= 1;
            _balances[to] += 1;
            _owners[tokenId] = to;
            emit Transfer(from, to, tokenId);
        }
    
        // 实现IERC721的transferFrom，非安全转账，不建议使用。调用_transfer函数
        function transferFrom(address from,address to,uint tokenId) external override {
            address owner = ownerOf(tokenId);
            require(
                _isApprovedOrOwner(owner, msg.sender, tokenId),
                "not owner nor approved"
            );
            _transfer(owner, from, to, tokenId);
        }
    
        /**
         * 安全转账，安全地将 tokenId 代币从 from 转移到 to，会检查合约接收者是否了解 ERC721 协议，以防止代币被永久锁定。调用了_transfer函数和_checkOnERC721Received函数。条件：
         * from 不能是0地址.
         * to 不能是0地址.
         * tokenId 代币必须存在，并且被 from拥有.
         * 如果 to 是智能合约, 他必须支持 IERC721Receiver-onERC721Received.
         */
        function _safeTransfer(address owner,address from,address to,uint tokenId,bytes memory _data) private {
            _transfer(owner, from, to, tokenId);
            require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver");
        }
    
        /**
         * 实现IERC721的safeTransferFrom，安全转账，调用了_safeTransfer函数。
         */
        function safeTransferFrom(address from,address to,uint tokenId,bytes memory _data) public override {
            address owner = ownerOf(tokenId);
            require(
                _isApprovedOrOwner(owner, msg.sender, tokenId),
                "not owner nor approved"
            );
            _safeTransfer(owner, from, to, tokenId, _data);
        }
    
        // safeTransferFrom重载函数
        function safeTransferFrom(address from,address to,uint tokenId) external override {
            safeTransferFrom(from, to, tokenId, "");
        }
    
        /** 
         * 铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to，同时释放Transfer事件。铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to，同时释放Transfer事件。
         * 这个mint函数所有人都能调用，实际使用需要开发人员重写，加上一些条件。
         * 条件:
         * 1. tokenId尚不存在。
         * 2. to不是0地址.
         */
        function _mint(address to, uint tokenId) internal virtual {
            require(to != address(0), "mint to zero address");
            require(_owners[tokenId] == address(0), "token already minted");
    
            _balances[to] += 1;
            _owners[tokenId] = to;
    
            emit Transfer(address(0), to, tokenId);
        }
    
        // 销毁函数，通过调整_balances和_owners变量来销毁tokenId，同时释放Transfer事件。条件：tokenId存在。
        function _burn(uint tokenId) internal virtual {
            address owner = ownerOf(tokenId);
            require(msg.sender == owner, "not owner of token");
    
            _approve(owner, address(0), tokenId);
    
            _balances[owner] -= 1;
            delete _owners[tokenId];
    
            emit Transfer(owner, address(0), tokenId);
        }
    
        // _checkOnERC721Received：函数，用于在 to 为合约的时候调用IERC721Receiver-onERC721Received, 以防 tokenId 被不小心转入黑洞。
        function _checkOnERC721Received(address from,address to,uint tokenId,bytes memory _data) private returns (bool) {
            if (to.isContract()) {
                return IERC721Receiver(to).onERC721Received(msg.sender,from,tokenId,_data) == 
                IERC721Receiver.onERC721Received.selector;
            } else {
                return true;
            }
        }
    
        /**
         * 实现IERC721Metadata的tokenURI函数，查询metadata。
         */
        function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
            require(_owners[tokenId] != address(0), "Token Not Exist");
    
            string memory baseURI = _baseURI();
            return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
        }
    
        /**
         * 计算{tokenURI}的BaseURI，tokenURI就是把baseURI和tokenId拼接在一起，需要开发重写。
         * BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ 
         */
        function _baseURI() internal view virtual returns (string memory) {
            return "";
        }
    }
    

[写一个免费铸造的APE](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#%E5%86%99%E4%B8%80%E4%B8%AA%E5%85%8D%E8%B4%B9%E9%93%B8%E9%80%A0%E7%9A%84ape)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------

我们利用`ERC721`来写一个免费铸造的`STAR APE`，总量设置为`10000`，只需要重写一下`mint()`和`baseURI()`函数即可。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.4;
    
    import "./ERC721.sol";
    
    contract STARApe is ERC721{
        uint public MAX_APES = 10000; // 总量
    
        // 构造函数
        constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
        }
    
        //BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ 
        function _baseURI() internal pure override returns (string memory) {
            return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
        }
        
        // 铸造函数
        function mint(address to, uint tokenId) external {
            require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range");
            _mint(to, tokenId);
        }
    }
    

[发行ERC721 NFT](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#%E5%8F%91%E8%A1%8Cerc721-nft)
------------------------------------------------------------------------------------------------------------------------

有了`ERC721`标准后，在ETH链上发行NFT变得非常简单。现在，我们发行属于我们的NFT。

在`remix`上编译好`ERC721`合约和`STARApe`合约（按照顺序），在部署栏点击下按钮，输入构造函数的参数，`name_`和`symbol_`都设置为`STAR`，然后点击`transact`键进行部署。

![](https://storage.googleapis.com/papyrus_images/f7c9ef634121a548498a8c9646e3aa0e178e1a8328369c918285799961b32bf1.png)

这样，我们就创建好了`STAR`的NFT。我们需要运行`mint()`函数来给自己铸造一些代币。在`mint`函数那一栏点开右侧的下按钮输入账户地址和tokenId，并点击`mint`按钮，为自己铸造`0`号`STAR` NFT。

可以点开右侧的Debug按钮，具体查看下面的logs。

里面包含四个关键信息：

*   事件`Transfer`
    
*   铸造地址`0x0000000000000000000000000000000000000000`
    
*   接收地址`0x5B38Da6a701c568545dCfcB03FcB875f56beddC4`
    
*   tokenid `0`
    

![](https://storage.googleapis.com/papyrus_images/2f47108a4f8950490090505884a5944b76c2f12973fe17c6c8c093a6f2d5502b.png)

我们利用`balanceOf()`来查询账户余额。输入我们当前的账户，可以看到有一个`NFT`，铸造成功。

账户信息如图左侧，右侧标注为函数执行的具体信息。

![](https://storage.googleapis.com/papyrus_images/4872daec53f1037ef36dfb5a67fa4e8afb46ee8c68ebf3a879c7d9edd34c8b00.png)

我们也可以利用`ownerOf()`函数来查询NFT属于哪个账户。输入`tokenId`，可以查询到地址。

![](https://storage.googleapis.com/papyrus_images/8c50884f6fb12043d31597566e5dfaf46392b8588d45a637f45aba05d028f447.png)

[ERC165与ERC721详解](https://github.com/starthapro/starha_solidity/blob/main/33_ERC721/readme.md#erc165%E4%B8%8Eerc721%E8%AF%A6%E8%A7%A3)
--------------------------------------------------------------------------------------------------------------------------------------

上面说到，为了防止NFT被转到一个没有能力操作NFT的合约中去，目标必须正确实现ERC721TokenReceiver接口：

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    // ERC721接收者接口：合约必须实现这个接口来通过安全转账接收ERC721
    interface IERC721Receiver {
        function onERC721Received(address operator,address from,uint tokenId,bytes calldata data) external returns (bytes4);
    }
    

拓展到编程语言的世界中去，无论是Java的interface，还是Rust的Trait(当然solidity中和trait更像的是library)，只要是和接口沾边的，都在透露着一种这样的意味：接口是某些行为的集合(在solidity中更甚，接口完全等价于函数选择器的集合)，某个类型只要实现了某个接口，就表明该类型拥有这样的一种功能。因此，只要某个contract类型实现了上述的`ERC721TokenReceiver`接口(更具体而言就是实现了`onERC721Received`这个函数),该contract类型就对外表明了自己拥有管理NFT的能力。当然操作NFT的逻辑被实现在该合约其他的函数中。 ERC721标准在执行`safeTransferFrom`的时候会检查目标合约是否实现了`onERC721Received`函数,这是一种利用ERC165思想进行的操作。

**那究竟什么是ERC165呢？**

ERC165是一种对外表明自己实现了哪些接口的技术标准。就像上面所说的，实现了一个接口就表明合约拥有了某种特殊能力。有一些合约与其他合约交互时，期望目标合约拥有某些功能，那么合约之间就能够通过ERC165标准对对方进行查询以检查对方是否拥有相应的能力。

以ERC721合约为例，当外部对某个合约进行检查其是否是ERC721时，[怎么做？](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165)按照这个说法，检查步骤应该是首先检查该合约是否实现了ERC165，再检查该合约实现的其他接口。测试特定接口是IERC721。IERC721是ERC721的基准接口（为什么说基准，是因为还有其他的诸如`ERC721Metadata`，`ERC721Enumerable`这样的扩展）：

    /// 注意这个**0x80ac58cd**
    ///  **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠**
    interface ERC721 /* is ERC165 */ {
        event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    
        event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    
        event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
    
        function balanceOf(address _owner) external view returns (uint256);
    
        function ownerOf(uint256 _tokenId) external view returns (address);
    
        function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
    
        function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    
        function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    
        function approve(address _approved, uint256 _tokenId) external payable;
    
        function setApprovalForAll(address _operator, bool _approved) external;
    
        function getApproved(uint256 _tokenId) external view returns (address);
    
        function isApprovedForAll(address _owner, address _operator) external view returns (bool);
    }
    

0x80a58cd = `bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^keccak256(ERC721.isApprovedForAll.selector))`，这是ERC165规定的计算方式。

那么，类似的，能计算出ERC165本身的接口（它的接口里只有一个`function supportsInterface(bytes4 interfaceID) external view returns (bool)`）函数，对其进行`bytes4(keccak256(supportsInterface.selector))` 得到**0x01ffc9a7**。此外，ERC721还定义了一些扩展接口，比如`ERC721Metadata`，长这样：

    ///  Note: the ERC-165 identifier for this interface is 0x5b5e139f.
    interface ERC721Metadata /* is ERC721 */ {
        function name() external view returns (string _name);
        function symbol() external view returns (string _symbol);
        function tokenURI(uint256 _tokenId) external view returns (string); // 这个很重要，前端展示的小图片的链接都是这个函数返回的
    }
    

这个**0x5b5e139f**的计算就是

    IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector
    

solamte实现的ERC721.sol是怎么完成这些ERC165要求的特性的呢？

    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
            return
                interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
                interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
                interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
    }
    

没错就这么简单。当外界按照[link1](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165)的步骤去做检查的时候，如果外界想检查这个合约是否实现了ERC165，就是supportsInterface函数在入参是`0x01ffc9a7`时必须返回true，在入参是`0xffffffff`时，返回值必须是false。上述实现完美达成要求。

当外界想检查这个合约是否是ERC721的时候，入参是0x80ac58cd 的时候表明外界想做这个检查。返回true。

当外界想检查这个合约是否实现ERC721的拓展ERC721Metadata接口时，入参是0x5b5e139f。好说，返回了true。

并且由于该函数是virtual的。因此该合约的使用者可以继承该合约，然后继续实现ERC721Enumerable 接口。实现完里面的什么totalSupply 啊之类的函数之后，把继承的supportsInterface重实现为

    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
            return
                interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
                interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
                interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata
                interfaceId == 0x780e9d63;   // ERC165 Interface ID for ERC721Enumerable
    }

---

*Originally published on [startha](https://paragraph.com/@starha/erc721)*
