# Solidity极简入门 ERC721专题：3. ERC721主合约

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

---

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

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

WTF技术社群discord，内有加微信群方法：[链接](https://discord.gg/5akcruXrsk)

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

不知不觉我已经完成了Solidity极简教程的前13讲（基础），内容包括：Helloworld.sol，变量类型，存储位置，函数，控制流，构造函数，修饰器，事件，继承，抽象合约，接口，库，异常。在进阶内容之前，我决定做一个`ERC721`的专题，把之前的内容综合运用，帮助大家更好的复习基础知识，并且更深刻的理解`ERC721`合约。希望在学习完这个专题之后，每个人都能发行自己的`NFT`。

* * *

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

我在ERC721前两讲介绍了它的相关库和接口，终于这讲可以介绍主合约了。ERC721主合约包含6个状态变量和28个函数，我们将会一一介绍。并且，我给ERC721代码增加了中文注释，方便大家使用。

### 状态变量

        // 代币名称
        string private _name;
     
        // 代币代号
        string private _symbol;
    
        // tokenId到owner地址Mapping
        mapping(uint256 => address) private _owners;
    
        // owner地址到持币数量Mapping
        mapping(address => uint256) private _balances;
    
        // tokenId到授权地址Mapping
        mapping(uint256 => address) private _tokenApprovals;
    
        // owner地址到是否批量批准Mapping
        mapping(address => mapping(address => bool)) private _operatorApprovals;
    

*   \_name和\_symbol是两个string，存储代币的名称和代号。
    
*   \_owners是tokenId到owner地址的Mapping，存储每个代币的持有人。
    
*   \_balances是owner地址到持币数量的Mapping，存储每个地址的持仓量。
    
*   \_tokenApprovals是tokenId到授权地址的Mapping，存储每个token的授权信息。
    
*   \_operatorApprovals是owner地址到是否批量批准的Mapping，存储每个owner的批量授权信息。**_注意，批量授权会把你钱包持有这个系列的所有nft都授权给另一个地址，别人可以随意支配。_**
    

### 函数

*   `constructor`：构造函数，设定`ERC721`代币的名字和代号（`_name`和`_symbol`变量）。
    

        constructor(string memory name_, string memory symbol_) {
            _name = name_;
            _symbol = symbol_;
        }
    

*   `supportsInterface`：实现`IERC165`接口`supportsInterface`，详见[ERC721专题第二讲](https://mirror.xyz/ninjak.eth/4mPkMgHViRjx8OM7TAI-M-2oMfRle36ULzqlpC6S7IQ)
    

        function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
            return
                interfaceId == type(IERC721).interfaceId ||
                interfaceId == type(IERC721Metadata).interfaceId ||
                super.supportsInterface(interfaceId);
        }
    

*   `balanceOf`：实现`IERC721`的`balanceOf`，利用`_balances`变量查询`owner`地址的`balance`。
    

        function balanceOf(address owner) public view virtual override returns (uint256) {
            require(owner != address(0), "ERC721: balance query for the zero address");
            return _balances[owner];
        }
    

*   `ownerOf`：实现`IERC721`的`ownerOf`，利用`_owners`变量查询`tokenId`的`owner`。
    

        function ownerOf(uint256 tokenId) public view virtual override returns (address) {
            address owner = _owners[tokenId];
            require(owner != address(0), "ERC721: owner query for nonexistent token");
            return owner;
        }
    

*   `name`：实现`IERC721Metadata`的`name`，查询代币名称。
    

        function name() public view virtual override returns (string memory) {
            return _name;
        }
    

*   `symbol`：实现`IERC721Metadata`的`symbol`，查询代币代号。
    

        function symbol() public view virtual override returns (string memory) {
            return _symbol;
        }
    

*   `tokenURI`：实现`IERC721Metadata`的`tokenURI`，查询代币`metadata`存放的网址。Opensea还有小狐狸钱包显示你`NFT`的图片，调用的就是这个函数。
    

        function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
            require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    
            string memory baseURI = _baseURI();
            return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
        }
    

*   `_baseURI`：基`URI`，会被`tokenURI()`调用，跟`tokenId`拼成`tokenURI`，默认为空，需要子合约重写这个函数。
    

        function _baseURI() internal view virtual returns (string memory) {
            return "";
        }
    

*   `approve`：实现`IERC721`的`approve`，将`tokenId`授权给 `to` 地址。条件：`to`不是`owner`，且`msg.sender`是`owner`或授权地址。调用`_approve`函数。
    

        function approve(address to, uint256 tokenId) public virtual override {
            address owner = ERC721.ownerOf(tokenId);
            require(to != owner, "ERC721: approval to current owner");
    
            require(
                _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
                "ERC721: approve caller is not owner nor approved for all"
            );
    
            _approve(to, tokenId);
        }
    

*   `getApproved`：实现`IERC721`的`getApproved`，利用`_tokenApprovals`变量查询`tokenId`的授权地址。
    

        function getApproved(uint256 tokenId) public view virtual override returns (address) {
            require(_exists(tokenId), "ERC721: approved query for nonexistent token");
    
            return _tokenApprovals[tokenId];
        }
    

*   `setApprovalForAll`：实现`IERC721`的`setApprovalForAll`，将持有代币全部授权给`operator`地址。调用`_setApprovalForAll`函数。
    

        function setApprovalForAll(address operator, bool approved) public virtual override {
            _setApprovalForAll(_msgSender(), operator, approved);
        }
    

*   `isApprovedForAll`：实现`IERC721`的`isApprovedForAll`，利用`_operatorApprovals`变量查询`owner`地址是否将所持NFT批量授权给了`operator`地址。
    

        function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
            return _operatorApprovals[owner][operator];
        }
    

*   `transferFrom`：实现`IERC721`的`transferFrom`，非安全转账，不建议使用。调用`_transfer`函数。
    

        function transferFrom(
            address from,
            address to,
            uint256 tokenId
        ) public virtual override {
            //solhint-disable-next-line max-line-length
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
    
            _transfer(from, to, tokenId);
        }
    

*   `safeTransferFrom`：实现`IERC721`的`safeTransferFrom`，安全转账，调用了`_safeTransfer`函数。
    

        function safeTransferFrom(
            address from,
            address to,
            uint256 tokenId,
            bytes memory _data
        ) public virtual override {
            require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
            _safeTransfer(from, to, tokenId, _data);
        }
    

*   `_safeTransfer`：安全转账，安全地将 `tokenId` 代币从 `from` 转移到 `to`，会检查合约接收者是否了解 ERC721 协议，以防止代币被永久锁定。调用了`_transfer`函数和`_checkOnERC721Received`函数。条件：
    
    *   `from` 不能是0地址.
        
    *   `to` 不能是0地址.
        
    *   `tokenId` 代币必须存在，并且被 `from`拥有.
        
    *   如果 `to` 是智能合约, 他必须支持 `IERC721Receiver-onERC721Received`.
        

        function _safeTransfer(
            address from,
            address to,
            uint256 tokenId,
            bytes memory _data
        ) internal virtual {
            _transfer(from, to, tokenId);
            require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
        }
    

*   `_exists`：查询 `tokenId`是否存在（等价于查询他的`owner`是否为非0地址）。
    

        function _exists(uint256 tokenId) internal view virtual returns (bool) {
            return _owners[tokenId] != address(0);
        }
    

*   `_isApprovedOrOwner`：查询 `spender`地址是否被可以使用`tokenId`（他是`owner`或被授权地址）。
    

        function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
            require(_exists(tokenId), "ERC721: operator query for nonexistent token");
            address owner = ERC721.ownerOf(tokenId);
            return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
        }
    

*   `_safeMint`：安全`mint`函数，铸造`tokenId`并转账给 `to`地址。 条件:
    
    *   `tokenId`尚不存在,
        
    *   若`to`为智能合约，他要支持`IERC721Receiver-onERC721Received`接口。
        

        function _safeMint(address to, uint256 tokenId) internal virtual {
            _safeMint(to, tokenId, "");
        }
    

*   `_safeMint`的实现，调用了`_checkOnERC721Received`函数和`_mint`函数。
    

        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"
            );
        }
    

*   `_mint`：`internal`铸造函数。通过调整`_balances`和`_owners`变量来铸造`tokenId`并转账给 `to`，同时释放`Tranfer`事件。条件:
    
    *   `tokenId`尚不存在。
        
    *   `to`不是0地址.
        

        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);
    
            _afterTokenTransfer(address(0), to, tokenId);
        }
    

*   `_burn`：`internal`销毁函数，通过调整`_balances`和`_owners`变量来销毁`tokenId`，同时释放`Tranfer`事件。条件：`tokenId`存在。
    

        function _burn(uint256 tokenId) internal virtual {
            address owner = ERC721.ownerOf(tokenId);
    
            _beforeTokenTransfer(owner, address(0), tokenId);
    
            // 清空授权
            _approve(address(0), tokenId);
    
            _balances[owner] -= 1;
            delete _owners[tokenId];
    
            emit Transfer(owner, address(0), tokenId);
    
            _afterTokenTransfer(owner, address(0), tokenId);
        }
    

*   `_transfer`：转账函数。通过调整`_balances`和`_owner`变量将 `tokenId` 从 `from` 转账给 `to`，同时释放`Tranfer`事件。条件:
    
    *   `tokenId` 被 `from` 拥有
        
    *   `to` 不是0地址
        

        function _transfer(
            address from,
            address to,
            uint256 tokenId
        ) internal virtual {
            require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
            require(to != address(0), "ERC721: transfer to the zero address");
    
            _beforeTokenTransfer(from, to, tokenId);
    
            // 清空授权
            _approve(address(0), tokenId);
    
            _balances[from] -= 1;
            _balances[to] += 1;
            _owners[tokenId] = to;
    
            emit Transfer(from, to, tokenId);
    
            _afterTokenTransfer(from, to, tokenId);
        }
    

*   `_approve`：授权函数。通过调整`_tokenApprovals`来，授权 `to` 地址操作 `tokenId`，同时释放`Approval`事件。
    

        function _approve(address to, uint256 tokenId) internal virtual {
            _tokenApprovals[tokenId] = to;
            emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
        }
    

*   `_setApprovalForAll`：批量授权函数。通过调整`_operatorApprovals`变量，批量授权 `to` 来操作 `owner`全部代币，同时释放`ApprovalForAll`事件。
    

        function _setApprovalForAll(
            address owner,
            address operator,
            bool approved
        ) internal virtual {
            require(owner != operator, "ERC721: approve to caller");
            _operatorApprovals[owner][operator] = approved;
            emit ApprovalForAll(owner, operator, approved);
        }
    

*   `_checkOnERC721Received`：函数，用于在 `to` 为合约的时候调用`IERC721Receiver-onERC721Received`, 以防 `tokenId` 被不小心转入黑洞。
    

        function _checkOnERC721Received(
            address from,
            address to,
            uint256 tokenId,
            bytes memory _data
        ) private returns (bool) {
            if (to.isContract()) {
                try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, _data) returns (bytes4 retval) {
                    return retval == IERC721Receiver.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;
            }
        }
    

*   `_beforeTokenTransfer`：这个函数在转账之前会被调用（包括`mint`和`burn`）。默认为空，子合约可以选择重写。
    

        function _beforeTokenTransfer(
            address from,
            address to,
            uint256 tokenId
        ) internal virtual {}
    

*   `_afterTokenTransfer`：这个函数在转账之后会被调用（包括`mint`和`burn`）。默认为空，子合约可以选择重写。
    

        function _afterTokenTransfer(
            address from,
            address to,
            uint256 tokenId
        ) internal virtual {}
    

总结
--

本文是`ERC721`专题的第三讲，我介绍了`ERC721`主合约的全部变量和函数，并给出了合约的中文注释。有了`ERC721`标准，NFT项目方只需要把`mint`函数包装一下，就可以发行NFT了。下一讲，我们会介绍无聊猿`BAYC`的合约，了解一下最火`NFT`在标准`ERC721`合约上做了什么改动。

---

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