# Solidity极简入门: 34. ERC721

By [0xAA](https://paragraph.com/@wtfacademy) · 2022-07-03

---

我最近在重新学solidity，巩固一下细节，也写一个“Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。

欢迎关注我的推特：[@0xAA\_Science](https://twitter.com/0xAA_Science)

欢迎加入WTF科学家社区，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

所有代码和教程开源在github（1024个star发课程认证，2048个star发社群NFT）: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity)

* * *

`BTC`和`ETH`这类代币都属于同质化代币，矿工挖出的第`1`枚`BTC`与第`10000`枚`BTC`并没有不同，是等价的。但世界中很多物品是不同质的，其中包括房产、古董、虚拟艺术品等等，这类物品无法用同质化代币抽象。因此，[以太坊EIP721](https://eips.ethereum.org/EIPS/eip-721)提出了`ERC721`标准，来抽象非同质化的物品。这一讲，我们将介绍`ERC721`标准，并基于它发行一款`NFT`。

ERC165
------

通过[ERC165标准](https://eips.ethereum.org/EIPS/eip-165)，智能合约可以声明它支持的接口，供其他合约检查。`IERC165`接口合约只声明了一个`supportsInterface`函数，输入要查询的`interfaceId`接口id，若合约实现了该接口id，则返回`true`：

    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()`函数的：

        function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
        {
            return
                interfaceId == type(IERC721).interfaceId ||
                interfaceId == type(IERC165).interfaceId;
        }
    

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

IERC721
-------

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

    /**
     * @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事件

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

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

### IERC721函数

*   `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
---------------

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

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

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

        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
---------------

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

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

    interface IERC721Metadata is IERC721 {
        function name() external view returns (string memory);
    
        function symbol() external view returns (string memory);
    
        function tokenURI(uint256 tokenId) external view returns (string memory);
    }
    

ERC721主合约
---------

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

    // SPDX-License-Identifier: MIT
    // by 0xAA
    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库，
    
        // Token名称
        string public override name;
        // Token代号
        string public override symbol;
        // tokenId 到 owner address 的持有人映射
        mapping(uint => address) private _owners;
        // address 到 持仓数量 的持仓量映射
        mapping(address => uint) private _balances;
        // tokenID 到 授权地址 的授权映射
        mapping(uint => address) private _tokenApprovals;
        //  owner地址。到operator地址 的批量授权映射
        mapping(address => mapping(address => bool)) private _operatorApprovals;
    
        /**
         * 构造函数，初始化`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;
        }
    
        // 实现IERC721的balanceOf，利用_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，同时释放Tranfer事件。
         * 条件:
         * 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，同时释放Tranfer事件。铸造函数。通过调整_balances和_owners变量来铸造tokenId并转账给 to，同时释放Tranfer事件。
         * 这个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，同时释放Tranfer事件。条件：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
-----------

我们来利用`ERC721`来写一个免费铸造的`WTF APE`，总量设置为`10000`，只需要重写一下`mint()`和`baseURI()`函数即可。由于`baseURI()`设置的和`BAYC`一样，元数据会直接获取无聊猿的，类似[RRBAYC](https://rrbayc.com/)：

    // SPDX-License-Identifier: MIT
    // by 0xAA
    pragma solidity ^0.8.4;
    
    import "./ERC721.sol";
    
    contract WTFApe 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`标准、接口及其实现，并在合约代码进行了中文注释。并且我们利用`ERC721`做了一个免费铸造的`WTF APE` NFT，元数据直接调用于`BAYC`。`ERC721`标准仍在不断发展中，目前比较流行的版本为`ERC721Enumerable`（提高NFT可访问性）和`ERC721A`（节约铸造`gas`）。

---

*Originally published on [0xAA](https://paragraph.com/@wtfacademy/solidity-34-erc721)*
