{"author":{"address":"0x1a8b97214f5bf508ef1889252032887b075d2e23","user":"https://learnblockchain.cn/people/14144"},"content":{"body":"### 1、ERC721标准规范\r\n\r\n#### 1.1 IERC721\r\n\r\n和`ERC20`一样，`ERC721`同样是一个代币标准，官方解释`NFT`为`Non-Fungible Token`,译作非同质化代币。\r\n\r\n如何理解`NFT`呢？非同质化代表独一无二，其和`ERC20`的区别，在于资产是否可以分割与独一无二，而`NFT`标准都有唯一的标识符和元数据，就像是世界上没有两片完全相同的树叶，每个`NFT`代币彼此不可替代，这些独特性使得`ERC721`标准具有广泛的应用场景，包括艺术品、收藏品、域名、数字证书、数字音乐等等领域。\r\n\r\n`ERC721`基本标准为\r\n\r\n- balanceOf()：返回`owner`账户的代币数量。\r\n\r\n- ownerOf()：返回`tokenId`的所有者。\r\n\r\n- safeTransferFrom()：安全转账`NFT`。\r\n\r\n  函数需要做以下校验\r\n\r\n  - `msg.senfer`应该是当前`tokenId`的`owner`或是`spender`；\r\n  - `_from`必须是`_tokenId`的所有者；\r\n  - `_tokenId`必须存在并且属于`_from`；\r\n  - `_to`如果是CA（合约地址）,它必须实现`IERC721Receiver-onERC721Received`接口，检查其返回值。这么做的目的是，为了避免将`tokenId`转移到一个无法控制的合约地址，导致`token`被永久转进黑洞。因为CA账户无法主动触发交易，只能由EOA账户来调用合约触发交易。\r\n\r\n- transferFrom()：非安全转账`NFT`。\r\n\r\n- approve()：授权地址`_to`具有`tokenId`的支配权\r\n\r\n- setApprovalForAll()：批准或取消`_openrater`的`token`操作权限，用于批量授权\r\n\r\n- getApproved()：获取`_tokenId`授权\r\n\r\n- isApprovedForAll()：获取`_tokenId`的支配情况\r\n\r\n#### 1.2 IERC165\r\n\r\n`ERC721`要求必须符合`ERC165标准`，什么是`ERC165`？\r\n\r\n和`ERC20`和`ERC721`一样，它也是以太坊系统的一种标准规范,其主要用于：\r\n\r\n- 一种接口检查查询和发布标准\r\n- 检测智能合约实现了哪些接口\r\n\r\n`IERC165`官方定义为\r\n\r\n```solidity\r\n\t/// @dev 查询一个合约时候实现了一个接口\r\n    /// param interfaceID  参数：接口ID\r\n    /// return true 如果函数实现了 interfaceID (interfaceID 不为 0xffffffff )返回true, 否则为 false\r\ninterface ERC165 {\r\n    function supportsInterface(bytes4 interfaceID) external view returns (bool);\r\n}\r\n```\r\n\r\n这里仅作补充，`ERC165`提供了一种确定性的互操作支持，方便了合约之间的检查交互。\r\n\r\n#### 1.3 IERC721与IERC165\r\n\r\n在`ERC721`当中，依赖`ERC165`接口，重写`supportsInterface(bytes4 interfaceId)`覆盖父级合约，在调用方法之前查询目标合约是否实现相应接口，具体实现如下：\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询目标合约是否实现ERC721接口\r\n     */\r\n    function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {\r\n        return\r\n            interfaceId == type(IERC721).interfaceId ||\r\n            interfaceId == type(IERC721Metadata).interfaceId ||\r\n            super.supportsInterface(interfaceId);\r\n    }\r\n```\r\n\r\n当查询的是`IERC721`、`IERC721Metadata`或`IERC165`的接口id时，返回`true`；反之返回`false`，参考：[EIP165官方提案](https://learnblockchain.cn/docs/eips/eip-165.html#实现)\r\n\r\n### 2、编写ERC721函数接口\r\n\r\n`IERC721`是`ERC721`标准的接口合约，规定了`ERC721`要实现的基本函数。它利用`tokenId`来表示特定的非同质化代币，授权或转账都要明确`tokenId`，它通过一组标准化的函数接口来管理资产的所有权和交易；而`ERC20`只需要明确转账的数额即可。\r\n\r\n#### 2.1 IERC165接口\r\n\r\n`IERC721`必须符合`IERC165`接口,便于合约交互做接口查询\r\n\r\n```solidity\r\ninterface IERC165 {\r\n    /**\r\n     * @dev 查询一个合约时候实现了一个接口\r\n     *\tparam interfaceID  参数：接口ID\r\n     *  return bool\r\n     */\r\n    function supportsInterface(bytes4 interfaceId) external view returns (bool);\r\n}\r\n```\r\n\r\n#### 2.2 ERC721事件\r\n\r\n`IERC721`定义了`3`个事件：`Transfer`、`Approval`和`ApprovalForAll`，分别在转账、授权和批量授权时候释放；\r\n\r\n```solidity\r\n \t/**\r\n     * @dev 释放条件：发生`tokenId`代币转移，从`from`转移至`to`.\r\n     * param( address , address , uint256 )\r\n     */\r\n    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);\r\n\r\n    /**\r\n     * @dev 释放条件：发生`tokenId`代币授权,`owner`授权给`approved`支配token.\r\n     * param( address , address , uint256 )\r\n     */\r\n    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);\r\n\r\n    /**\r\n     * @dev 释放条件：当`owner`管理`operator`的所有资产管理权限，即批量授权\r\n     * param(address,address,bool)\r\n     */\r\n    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);\r\n\r\n```\r\n\r\n#### 2.3 函数接口\r\n\r\n`IERC721`定义了9个函数，实现代币的交易、授权和查询功能。\r\n\r\n- `balanceOf()`\r\n\r\n返回目标账户`owner`的代币数量,区分`ERC20`代币数量,这里可理解为`tokenId`数量\r\n\r\n```solidity\r\n\t/**\r\n     * @dev 返回代币数量.\r\n     * param address 账户地址\r\n     * return uint256 代币数量\r\n     */\r\n    function balanceOf(address owner) external view returns (uint256 balance);\r\n\r\n```\r\n\r\n- `ownerOf()`\r\n\r\n返回`tokenId`的`owner`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询`tokenId`的拥有者\r\n     * \r\n     *  param uint256 tokenId\r\n     *  return address 代币拥有者\r\n     * 查询条件:\r\n     * - `tokenId` 必须存在.\r\n     */\r\n    function ownerOf(uint256 tokenId) external view returns (address owner);\r\n    \r\n```\r\n\r\n- `safeTransferFrom()`\r\n\r\n安全转账，将`tokenId`从`from`转移至`to`，携带`data`参数,`data`的作用可以是附加额外的参数（没有指定格式），传递给接收者。\r\n\r\n```solidity\r\n    /**\r\n     * @dev 安全转账,将NFT的所有权从`from`转移至`to`.\r\n     *\r\n     * 转移条件:\r\n     *\r\n     * - `from` 不能是address(0).\r\n     * - `to` 不能是address(0).\r\n     * - `tokenId` 必须存在且属于`from`.\r\n     * - 如果调用者不是`from`,则必须通过授权校验，拥有该`tokenId`的支配权.\r\n     * - 如果`to`为合约地址，则必须实现{IERC721Receiver-onERC721Received}接口.\r\n     *\r\n     *释放 {Transfer} 事件.\r\n     */\r\n    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;\r\n    \r\n```\r\n\r\n- `safeTransferFrom()`\r\n\r\n安全转账， 将`tokenId`从`from`转移至`to`，功能同上，不带data参数。\r\n\r\n```solidity\r\n    /**\r\n     * @dev 功能参考 ``safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data)``\r\n     *\r\n     * 释放 {Transfer} 事件.\r\n     */\r\n    function safeTransferFrom(address from, address to, uint256 tokenId) external;\r\n    \r\n```\r\n\r\n- `transferFrom()`\r\n\r\n普通转账， 将`tokenId`从`from`转移至`to`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 转移 `tokenId` 从 `from` 到 `to`.\r\n     *\r\n     * @notice: 调用此方法需注意接收者有能力调配`ERC721`，否则可能会永久丢失，推荐使用`safeTransferFrom`，但这会增加一次外部调用，可能会导致重入，注意防范.\r\n     *\r\n     * 条件:参考`safeTransferFrom`\r\n     *\r\n     * 释放 {Transfer} 事件.\r\n     */\r\n    function transferFrom(address from, address to, uint256 tokenId) external;\r\n\r\n```\r\n\r\n- `approve()`\r\n\r\n代币授权，和`ERC20`一致，授权其他用户支配自己的代币，这里体现为`NFT`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 授权`to`账户支配调用者`msg.sender`的`tokenId`-`NFT`权限.\r\n     * 当`token`发生转账时会清除授权.\r\n     *\r\n     * NFT只能授权给一个账户，当发生新的授权时候会更新授权账户.\r\n     *\r\n     * 条件:\r\n     *\r\n     * - 调用者必须为拥有该`NFT`或者被授权能够支配该`NFT`\r\n     * - `tokenId` 必须存在.\r\n     *\r\n     * 释放 {Approval} 事件.\r\n     */\r\n    function approve(address to, uint256 tokenId) external;\r\n    \r\n```\r\n\r\n- `setApprovalForAll()`\r\n\r\n批量授权其他账户支配自己NFT的权限\r\n\r\n```solidity\r\n    /**\r\n     * @dev 批准或者移除`operator`账户对`msg.sender`账户所有NFT操作的权限\r\n     * operator可以调用{transferFrom}或者{safeTransferFrom}转移token\r\n     *\r\n     * 条件:\r\n     *\r\n     * - `operator` 不能是address(0).\r\n     *\r\n     * 释放 {ApprovalForAll} 事件.\r\n     */\r\n    function setApprovalForAll(address operator, bool approved) external;\r\n\r\n```\r\n\r\n- `getApproved()`\r\n\r\n查询某`tokenId`被授权给哪个账户\r\n\r\n```solidity\r\n    /**\r\n     * @dev 返回`tokenId`批准支配的账户.\r\n     *\r\n     * 条件:\r\n     *\r\n     * - `tokenId` 必须存在.\r\n     */\r\n    function getApproved(uint256 tokenId) external view returns (address operator);\r\n    \r\n```\r\n\r\n- `isApprovedForAll()`\r\n\r\n查询某地址的NFT是否批量授权给了operator`地址支配\r\n\r\n```solidity\r\n    /**\r\n     * @dev 返回是否允许`operator`能够支配`owner`的所有NFT\r\n     *\r\n     */\r\n    function isApprovedForAll(address owner, address operator) external view returns (bool);\r\n```\r\n\r\n### 3、 编写IERC721Metadata接口\r\n\r\n`IERC721Metadata`是`IERC721`的扩展接口，实现了`ERC721`的元数据扩展，包括`name`、`symbo`、和`tokenURI`（`NFT`所对应的资源）。该接口用于存储额外数据，包括：\r\n\r\n- `name()`：返回代币名称\r\n- `symbol()`：返回代币代号符号\r\n- `tokenURI()`：返回`tokenId`对应的元数据，URI通常存储图片的链接路径或者是`IPFS`存储链接\r\n\r\n其**接口标准**为\r\n\r\n```solidity\r\n/**\r\n * @title ERC-721 元数据扩展接口\r\n * @dev 见 https://eips.ethereum.org/EIPS/eip-721\r\n */\r\ninterface IERC721Metadata is IERC721 {\r\n    /**\r\n     * @dev 查询代币名称.\r\n     */\r\n    function name() external view returns (string memory);\r\n\r\n    /**\r\n     * @dev 查询代币代号符号.\r\n     */\r\n    function symbol() external view returns (string memory);\r\n\r\n    /**\r\n     * @dev 查询NFT的URI元数据\r\n     */\r\n    function tokenURI(uint256 tokenId) external view returns (string memory);\r\n}\r\n```\r\n\r\n### 4、 编写IERC721Receiver接口\r\n\r\n`IERC721Receiver`确保合约地址如果要接收`NFT`的安全转账，必须实现其接口。这里在于提醒合约开发者在编写接收`NFT`的合约时候，能够编写有效处理`NFT`的转账逻辑.\r\n\r\n这里可以理解为当`EOA`转账`ETH`给`CA`时，如果合约代码没有实现`withdraw`函数进行提现`eth`，那么这个`eth`便永久的储存在合约当中，因为合约代码不能自发调用其代码，必须通过`EOA`账户进行函数调用。对比`NFT`的转账，如果开发者在接收`NFT`的合约中没有提供转账`NFT`的功能，那么这个`token`便会永久留在这个合约当中，相当于发送进黑洞。\r\n\r\n为了防止这种情况，`IERC721Receiver`接口中包含`onERC721Received`函数，只用接收合约中实现了这个接口才能接收`NFT`，意味着开发者意识到了这个问题，在自己的合约代码中防范了这种情况，当然，如果实现了这个接口，但是仍然没有针对合约转账`NFT`做出防范措施，那头铁的结果依然是`NFT`进了黑洞。\r\n\r\n`IERC721Receiver`标注规范为：\r\n\r\n```solidity\r\ninterface IERC721Receiver {\r\n    /**\r\n     * @dev 当发送想合约转账NFT时，回调此函数\r\n     *\r\n     * @notice 返回其函数选择器，以确认token转账.\r\n     * @notice 返回其他值，或者接收合约未实现该接口，转账将被revert.\r\n     *\r\n     * 函数选择器可通过`IERC721Receiver.onERC721Received.selector`获得.\r\n     */\r\n    function onERC721Received(\r\n        address operator,\r\n        address from,\r\n        uint256 tokenId,\r\n        bytes calldata data\r\n    ) external returns (bytes4);\r\n}\r\n```\r\n\r\n### 5、 编写IERC721Errors错误接口\r\n\r\n`IERC721Errors`定义了`8`个错误，帮助我们在实现代码业务逻辑时捕获错误异常\r\n\r\n- `ERC721InvalidOwner`\r\n\r\n转账错误时候触发，表明NFT的owner地址不合法\r\n\r\n```solidity\r\n    /**\r\n     * @dev 不合法的owner地址. 例如:address(0).\r\n     * 用于查询balance时候调用.\r\n     * param address -- owner.\r\n     */\r\n    error ERC721InvalidOwner(address owner);\r\n    \r\n```\r\n\r\n- `ERC721NonexistentToken`\r\n\r\n`tokenId`不存在时候触发\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明 `tokenId`的`owner`为address(0).\r\n     * param uint256 -- tokenId.\r\n     */\r\n    error ERC721NonexistentToken(uint256 tokenId);\r\n    \r\n```\r\n\r\n- `ERC721IncorrectOwner`\r\n\r\n`tokenId`对应的token所有权错误，转账时候触发\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明 `tokenId`的`owner`为发生错误.\r\n     * param (address,tokenId,address) -- (发送方，tokenId，NFTowner).\r\n     */\r\n    error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);\r\n    \r\n```\r\n\r\n- `ERC721InvalidSender`\r\n\r\n`token`转账错误，不合法的`sender`，多用于address(0)转账NFT\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明`sender`发送token失败\r\n     * param address 发生转账NFT的地址.\r\n     */\r\n    error ERC721InvalidSender(address sender);\r\n\r\n```\r\n\r\n- `ERC721InvalidReceiver`\r\n\r\n`token`转账错误，不合法的`receiver`，多用于向address(0)转账NFT\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明`receiver`接收token失败\r\n     * param address 接收转账NFT的地址.\r\n     */\r\n    error ERC721InvalidReceiver(address receiver);\r\n    \r\n```\r\n\r\n- `ERC721InsufficientApproval`\r\n\r\n`operator`操作账户获取授权失败，表明未被授权`tokenId`的操作权限\r\n\r\n```solidity\r\n    /**\r\n     * @dev `operater`未经授权`tokenId`，转账失败.\r\n     * param (address uint256) -- (操作账户，`tokenId`)\r\n     *\r\n     */\r\n    error ERC721InsufficientApproval(address operator, uint256 tokenId);\r\n\r\n```\r\n\r\n- `ERC721InvalidApprover`\r\n\r\n参考转账账户发生的错误，这里用于授权账户不合法,即address(0)发生授权。授权的时候触发\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明授权账户`approver`不合法，.\r\n     * param address -- 授权账户.\r\n     */\r\n    error ERC721InvalidApprover(address approver);\r\n    \r\n```\r\n\r\n- `ERC721InvalidOperator`\r\n\r\n操作账户不合法，即向address(0)地址授权。授权时候触发\r\n\r\n```solidity\r\n    /**\r\n     * @dev 表明操作账户`operator`不合法，.\r\n     * param address -- 操作账户.\r\n     */\r\n    error ERC721InvalidOperator(address operator);\r\n    \r\n```\r\n\r\n`8`个`error`，涵盖了在`NFT`发生转账、授权的时候可能遇到的错误，帮助我们在编写代码的时候捕获错误异常\r\n\r\n### 6、 实现ERC721\r\n\r\n`ERC721`主合约实现了`IERC721`，`IERC165`和`IERC721Metadata`，`IERCErrors`定义的所有功能,此外我们借助`Openzeppelin`的[Strings.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol)方法帮助我们处理`uint256`类型的字符串转换问题\r\n\r\n接下来我们创建ERC721合约，导入以下接口文件\r\n\r\n```solidity\r\nimport {IERC721} from \"./IERC721.sol\";\r\nimport {IERC721Metadata} from \"./IERC721Metadata.sol\";\r\nimport {IERC721Receiver} from \"./IERC721Receiver.sol\";\r\nimport {IERC165} from \"./IERC165.sol\";\r\nimport {IERC721Errors} from \"./IERC721Errors.sol\";\r\nimport \"@openzeppelin/contracts/utils/Strings.sol\";\r\n\r\n```\r\n\r\n#### 6.1 状态变量\r\n\r\n对比`ERC20`标准，我们同样需要使用状态变量来记录账户`NFT`信息，授权以及`token`信息\r\n\r\n```solidity\r\n    using  Strings for uint256;\r\n\r\n    // 代币名称\r\n    string private _name;\r\n    // 代币符号\r\n    string private _symbol;\r\n    // NFT 的owner\r\n    mapping (uint256  tokenId =\u003e address) private _owner;\r\n    // 账户拥有的的NFT数量\r\n    mapping (address owner =\u003e uint256) private  _balances;\r\n    // NFT的授权账户\r\n    mapping (uint256 tokenId =\u003e address) private _tokenApprovals;\r\n    // 账户operator 是否被授权支出 owner 的NFT，即批量授权\r\n    mapping (address owner =\u003e mapping (address operator =\u003e bool)) private  _operatorApprovals;\r\n```\r\n\r\n#### 6.2 函数\r\n\r\n- 构造函数：初始化代币名称，符号。\r\n\r\n```solidity\r\n    /**\r\n     * @dev 合约部署时实例化name 和 symbol 状态变量.\r\n     */\r\n    constructor(string memory name_ , string memory symbol_){\r\n        _name = name_;\r\n        _symbol = symbol_;\r\n    }\r\n```\r\n\r\n- `supportsInterface()`\r\n\r\n查询`NFT`合约支持的接口，在调用方法之前查询目标合约是否实现相应接口，详细描述见`1.3`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询接口ID.\r\n     */\r\n    function supportsInterface(bytes4 interfaceId)public  pure returns (bool){\r\n        return \r\n            interfaceId == type(IERC721).interfaceId ||\r\n            interfaceId == type(IERC721Metadata).interfaceId ||\r\n            interfaceId == type(IERC165).interfaceId;\r\n    }\r\n    \r\n```\r\n\r\n- `balanceOf()`\r\n\r\n查询`owner`持有的代币数量\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询用户持仓数量.\r\n     */\r\n    function balanceOf(address owner) public  view returns (uint256){\r\n        //地址校验\r\n        if (owner == address(0)){\r\n            revert ERC721InvalidOwner(address(0));\r\n        }\r\n        return _balances[owner];\r\n    }\r\n    \r\n```\r\n\r\n- `ownerOf()和_requireOwned()`\r\n\r\n查询`tokenId`的所有者,校验`NFT`是否存在\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询NFT的所有者.\r\n     */\r\n    function _ownerOf(uint256 tokenId)internal  view returns (address){\r\n        return _owner[tokenId];\r\n    }\r\n    /**\r\n     * @dev 供外部调用，逻辑处理交给_ownerOf().\r\n     */\r\n    function ownerOf(uint256 tokenId)public  view  returns (address){\r\n        return _requireOwned(tokenId);\r\n    }\r\n    /**\r\n     * @dev 如果 `tokenId` 没有当前所有者（尚未铸币或已被烧毁） 交易回滚\r\n     * 返回 `owner`.\r\n     */\r\n    function _requireOwned(uint256 tokenId)internal view returns(address){\r\n        //判断NFT是否存在\r\n        address owner = _ownerOf(tokenId);\r\n        if (owner == address(0)){\r\n            revert ERC721NonexistentToken(tokenId);\r\n        }\r\n        return owner;\r\n\r\n    }\r\n```\r\n\r\n- `name()`和`symbol()`\r\n\r\n查询NFT的名称和代号\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询名称.\r\n     */\r\n    function name()public  view returns (string memory){\r\n        return _name;\r\n    }\r\n    /**\r\n     * @dev 查询代号.\r\n     */\r\n    function symbol()public  view  returns (string memory){\r\n        return  _symbol;\r\n    }\r\n```\r\n\r\n- `tokenURI()`和`_baseURI()`\r\n\r\n查询`NFT`的`URI`元数据\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询NFT扩展对应外部资源.\r\n     */\r\n    function tokenURI(uint256 tokenId) public  view  returns (string memory){\r\n        //这里做NFT校验\r\n        _requireOwned(tokenId);\r\n        //获取基础URI\r\n        string memory baseURI = _baseURI();\r\n\r\n        //拼接{baseURI} + {tokenId}\r\n        return bytes(baseURI).length \u003e 0 ? string.concat(baseURI,tokenId.toString()) : \"\";\r\n    }\r\n\r\n    /**\r\n     * @dev 用作{tokenURI} 的基础 URI \r\n     * 如果设置：\r\n     * 每个{token}的URI 由`baseURI` + `tokenId`拼接而成\r\n     *   \r\n     * 这里默认为 \"\" 支持后续继承重载\r\n     */\r\n    function _baseURI()internal pure virtual returns (string memory){\r\n        return  \"\";\r\n    } \r\n```\r\n\r\n- `_getApproved()`\r\n\r\n查询`NFT`的授权地址\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询`tokenId` 的授权账户. 未被授权则返回address(0)\r\n     */\r\n    function _getApproved(uint256 tokenId) internal  view  returns (address){\r\n        return _tokenApprovals[tokenId];\r\n    }\r\n```\r\n\r\n- `_isAuthorized()`\r\n\r\n查询`tokenId`的NFT的账户操作权限\r\n\r\n```solidity\r\n    /**\r\n     * @dev 查询`spender`是否能操作`owner`的NFT\r\n     * 三种情况：1.spender是NFT的owner 2. spender被owner批量授权管理其NFT 3.spender被owner授权管理`tokenId`的NFT\r\n     */\r\n    function _isAuthorized(address owner ,address spender , uint256 tokenId) internal view returns (bool){\r\n        return \r\n            spender != address(0) \u0026\u0026\r\n            (owner == spender || _operatorApprovals[owner][spender] || _getApproved(tokenId) == spender);\r\n    }\r\n```\r\n\r\n- `_checkAuthorized()`\r\n\r\n检查NFT授权情况，捕获相应错误\r\n\r\n```solidity\r\n    /**\r\n     * @dev 检查`spender`是否能操作`owner`的NFT\r\n     * 捕获相应错误{ERC721NonexistentToken} {ERC721InsufficientApproval}\r\n     */\r\n    function _checkAuthorized(address owner , address spender , uint256 tokenId)internal  view {\r\n        if (!_isAuthorized(owner, spender, tokenId)){\r\n            //这里的owner一般是后续外部调用： 通过`tokenId`查询得到的地址，即使用`_ownerOf()`函数得到的地址，所以非捕获的{ERC721InvalidOwner}错误\r\n            if (owner  == address(0)){\r\n                revert ERC721NonexistentToken(tokenId);\r\n            }else {\r\n                revert ERC721InsufficientApproval(spender,tokenId);\r\n            }\r\n        }\r\n    }\r\n```\r\n\r\n- `_approve()`\r\n\r\n授权逻辑，参考`ERC20`的授权函数入参，这里同样引入`emitEvent`来区分是否需要释放授权事件\r\n\r\n```solidity\r\n    /**\r\n     * @dev 授权内部处理逻辑 emitEvent可选\r\n     *\r\n     * @param to 授权地址\r\n     * @param tokenId NFT id\r\n     * @param auth 支出账户 \r\n     * @param emitEvent 事件释放信号\r\n     */\r\n    function _approve(address to , uint256 tokenId , address auth , bool emitEvent)internal {\r\n        //地址校验\r\n        if (emitEvent || auth != address(0)){\r\n            address owner = _requireOwned(tokenId);\r\n            \r\n            //权限判断，授权账户非address(0)情况下：owner和auth不等且未获得批量授权;\r\n            if (auth != address(0) \u0026\u0026 owner != auth \u0026\u0026 !_operatorApprovals[owner][auth]){\r\n                 revert ERC721InvalidApprover(auth);\r\n            }\r\n\r\n            if ( emitEvent ){\r\n                emit  Approval(owner, to, tokenId);\r\n            }\r\n        }\r\n        //更新授权\r\n        _tokenApprovals[tokenId] = to;\r\n    }\r\n```\r\n\r\n- `_setApprovalForAll()`\r\n\r\n批量授权逻辑\r\n\r\n```solidity\r\n    /**\r\n     * @dev 批量授权owner的NFT\r\n     *\r\n     * 条件:\r\n     * - operator 不能是address(0).\r\n     *\r\n     *  释放{ApprovalForAll} 事件.\r\n     */\r\n    function _setApprovalForAll(address owner , address operator , bool approved) internal {\r\n        //地址校验\r\n        if (operator == address(0)){\r\n            revert ERC721InvalidOperator(operator);\r\n        }\r\n\r\n        _operatorApprovals[owner][operator] = approved;\r\n        emit ApprovalForAll(owner, operator, approved);\r\n    }\r\n```\r\n\r\n- `_checkOnERC721Received()`\r\n\r\n在安全转账`NFT`的时候调用，检查如果接收账户为CA则检查目标合约是否实现`IERC721Receiver`接口，提醒开发者注意合约是否能够正确处理转入合约的`NFT`，而`_checkOnERC721Received`内部检查目标合约是否返回指定的接口ID\r\n\r\n```solidity\r\n    /**\r\n     * @dev 在目标地址上调用 {IERC721Receiver-onERC721Received}数。如果\r\n     * 接收方不接受token转账。如果目标地址不是合约，则不执行调用。\r\n     *\r\n     * @param from 地址，代表给定token ID 的上一个所有者\r\n     * @param to 将接收代币的目标地址\r\n     * @param tokenId uint256 要传输的NFT\r\n     * @param data bytes 可选数据，与调用一起发送\r\n     */\r\n    function  _checkOnERC721Received(address from , address to ,uint256 tokenId ,bytes memory data)private {\r\n        //在 {address} 的代码，EOA为空\r\n        if (to.code.length \u003e 0){\r\n            try IERC721Receiver(to).onERC721Received(msg.sender , from , tokenId , data) returns (bytes4 retval){\r\n                //校验返回值和指定ID是否一致\r\n                if (retval != IERC721Receiver.onERC721Received.selector){\r\n                    revert ERC721InvalidReceiver(to);\r\n                }\r\n            }catch (bytes memory reason ) {\r\n                if (reason.length == 0){\r\n                    revert ERC721InvalidReceiver(to);\r\n                }else {\r\n                    assembly {\r\n                        //这里简单介绍：\r\n                        //reason指向存储错误消息的指针位置\r\n                        //add(32,reason)指针向前移动32个字节，因为Solidity动态数组在内存存储时候，前32个字节用于存储数组的长度\r\n                        //这里加上32个字节，指针跳过数组长度信息，直接指向错误消息的实际内容\r\n\r\n                        //mload指令： 从内存中加载数据\r\n                        //从内存中reason + 32的位置开始，以mload(reason)指定的长度来返回错误消息，并终止交易\r\n                        revert(add(32,reason),mload(reason))\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n```\r\n\r\n- `_update()`\r\n\r\n转账的内部处理逻辑，包含用户转账、`NFT`铸造、`NFT`销毁等，都可视作`NFT`的转账，区别在于转账账户的不同。\r\n\r\n```solidity\r\n    /**\r\n     * @dev 将 `tokenId` 从其当前拥有者转移到 `to` 中，或者，如果当前拥有者或 `to` 是零地址，则进行铸币（或烧毁）\r\n     *       \r\n     * `auth` \"参数是可选参数。如果传递的值非零地址，则此函数将检查\r\n     * `auth` 是`token`的所有者，或已获准对`token`进行操作（由所有者批准）\r\n     *\r\n     * 释放 {Transfer} 事件。\r\n     *\r\n     */\r\n    function _update(address to , uint256 tokenId , address auth)internal returns (address){\r\n        //获取NFT的owner\r\n        address from = _ownerOf(tokenId);   \r\n\r\n        //地址校验 \u0026\u0026 检查auth是否有支出权限 \r\n        if (auth != address(0)) {\r\n            _checkAuthorized(from, auth, tokenId);\r\n        }\r\n\r\n        //执行转账逻辑，首先判断NFT\r\n        if (from != address(0)){\r\n            //更新授权 授权账户清除\r\n            _approve(address(0), tokenId, address(0), false);\r\n            //更新from持仓数量\r\n            unchecked {\r\n                _balances[from] -= 1;\r\n            }\r\n        }\r\n        //to 如果不是零地址 则代表不是销毁\r\n        if (to != address(0)){\r\n            //更新to持仓数量\r\n            unchecked{\r\n                _balances[to] += 1 ;\r\n            }\r\n        }\r\n        //更新NFT所有者\r\n        _owner[tokenId] = to;\r\n\r\n        emit Transfer(from, to, tokenId);\r\n\r\n        return  from;\r\n    }\r\n```\r\n\r\n- `_mint()`\r\n\r\n铸造`NFT`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 为 `tokenId` 造币并将其传输到 `to`。\r\n     *\r\n     * 建议使用 {_safeMint}\r\n     *\r\n     * 要求：\r\n     *\r\n     * `tokenId` 必须不存在。\r\n     * `to` 不能是零地址。\r\n     *\r\n     * 释放 {Transfer} 事件。\r\n     */\r\n    function _mint(address to , uint256 tokenId) internal {\r\n        if (to == address(0)){\r\n            revert ERC721InvalidReceiver(address(0));\r\n        }\r\n        //判断前置NFT的Owner , 如果未铸造账户应是零地址\r\n        address _previousOwner = _update(to, tokenId, address(0));\r\n\r\n        if (_previousOwner != address(0) ){\r\n            revert ERC721InvalidSender(address(0));\r\n        }\r\n    }\r\n```\r\n\r\n- `_safeMint()`\r\n\r\n安全铸造`NFT`，校验接收账户的NFT处理\r\n\r\n```solidity\r\n    /**\r\n     * @dev 安全铸造NFT，接收方若为合约地址则进行接口ID校验\r\n     *\r\n     *  详细解释参考{_checkOnERC721Received}\r\n     */\r\n    function _safeMint(address to, uint256 tokenId )internal {\r\n        _mint(to, tokenId);\r\n        _checkOnERC721Received(address(0), to, tokenId, \"\");\r\n    }\r\n```\r\n\r\n- `_burn()`\r\n\r\n销毁`NFT`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 为 `tokenId` 销毁，看作将其传输到 `address(0)`。\r\n     *\r\n     * 要求：\r\n     *\r\n     * `tokenId` 必须存在\r\n     *\r\n     * 释放 {Transfer} 事件。\r\n     */\r\n    function _burn(uint256 tokenId)internal {\r\n        address previousOwner = _update(address(0), tokenId, address(0));\r\n        if (previousOwner == address(0)){\r\n            revert ERC721NonexistentToken(tokenId);\r\n        }\r\n    }\r\n```\r\n\r\n- `transferFrom()`\r\n\r\n转账`NFT`，用户转账自己的`NFT`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 将 `tokenId` 从 `from` 传输到 `to`，与 {transferFrom} 相反，这对 msg.sender 没有任何限制。\r\n     *\r\n     * 要求：\r\n     * -`to` 不能是零地址。\r\n     * -`tokenId` 必须为 `from` 所有\r\n     *\r\n     * 释放 {Transfer} 事件\r\n     */\r\n    function transferFrom(address from , address to , uint256 tokenId)public {\r\n        if (to == address(0)){\r\n             revert ERC721InvalidReceiver(address(0));\r\n        }\r\n        //执行转账逻辑\r\n        address previousOwner = _update(to, tokenId, msg.sender);\r\n        //返回NFT的owner值校验\r\n        //如果为address(0) 则NFT不存在\r\n        if (previousOwner == address(0)){\r\n             revert ERC721NonexistentToken(tokenId);\r\n        //如果owner和from不等，则转账NFT错误\r\n        }else if (previousOwner != from){\r\n            revert ERC721IncorrectOwner(from, tokenId, previousOwner);\r\n        }\r\n\r\n    }\r\n```\r\n\r\n- `safeTransferFrom()`\r\n\r\n安全转账`NFT`\r\n\r\n```solidity\r\n    /**\r\n     * @dev 安全地将 `tokenId` 令牌从 `from` 传输到 `to`，检查合约接收方,防止代币被永久锁定。\r\n     *\r\n     * `data` 是附加数据，没有指定格式，在调用 `to` 时发送。\r\n     *\r\n     * 要求：\r\n     *\r\n     * -`tokenId` 令牌必须存在并为 `from` 所有。\r\n     * - `to` 不能是零地址。\r\n     * - `from`不能是零地址。\r\n     * - 如果 `to` 指向一个智能合约，它必须实现 {IERC721Receiver-onERC721Received}，在安全转移时调用。\r\n     */\r\n    function safeTransferFrom(address from , address to ,uint256 tokenId)public  {\r\n        safeTransferFrom(from, to, tokenId, \"\");\r\n    }\r\n\r\n    /**\r\n     * 与[`safeTransferFrom`]相同，但多了一个`data`参数。\r\n     */\r\n    function safeTransferFrom(address from, address to,uint256 tokenId , bytes memory data)public {\r\n        transferFrom(from, to, tokenId);\r\n        _checkOnERC721Received(from, to, tokenId, data);\r\n    }\r\n```\r\n\r\n- `approve()`\r\n\r\n授权函数\r\n\r\n```solidity\r\n    /**\r\n     *  实现{IERC721}, 调用内部处理逻辑\r\n     *  释放 {Approval} 事件\r\n     */\r\n    function approve(address to ,uint256 tokenId)public  {\r\n        _approve(to, tokenId, msg.sender, true);\r\n    }\r\n```\r\n\r\n- `getApproved()`\r\n\r\n查询授权账户\r\n\r\n```solidity\r\n    /**\r\n     *  实现{IERC721-getApproved}, 调用内部处理逻辑\r\n     *  \r\n     */\r\n    function getApproved(uint256 tokenId)public view returns (address){\r\n        //确保NFT存在\r\n        _requireOwned(tokenId);\r\n\r\n        return _getApproved(tokenId);\r\n\r\n    }\r\n```\r\n\r\n- `setApprovalForAll()`\r\n\r\n进行批量授权\r\n\r\n```solidity\r\n    /**\r\n     *  实现{IERC721-setApprovalForAll}, 调用内部处理逻辑\r\n     *  释放 {ApprovalForAll} 事件\r\n     */\r\n    function setApprovalForAll(address operator , bool approved)public {\r\n        _setApprovalForAll(msg.sender, operator, approved);\r\n    }\r\n```\r\n\r\n- `isApprovedForAll()`\r\n\r\n查询`operator`是否获得`owner`账户批量授权`NFT`\r\n\r\n```solidity\r\n    /**\r\n     *  实现{IERC721-isApprovedForAll}, 调用内部处理逻辑\r\n     * \r\n     */\r\n    function isApprovedForAll(address owner , address operator) public  view  returns (bool) {\r\n        return  _operatorApprovals[owner][operator];\r\n    }\r\n```\r\n\r\n### 7、 发行NFT\r\n\r\n我们来利用`ERC721`来写一个免费铸造的`NFT`：\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity ^0.8.0;\r\n\r\nimport \"./ERC721.sol\";\r\n\r\ncontract NFT is ERC721 {\r\n    uint256 public counters = 1;\r\n\r\n    constructor()ERC721(\"NFT\",\"NFT\") {\r\n\r\n    }\r\n    function mint(address to )public {\r\n        _mint(to, counters);\r\n        counters++;\r\n    }\r\n}\r\n```\r\n\r\n总结：ERC721的剖析我们就到这里，NFT还有很多优秀的设计模式，包括：\r\n\r\n- `ERC721Enumerable`：支持对ERC721持有的代币进行枚举；\r\n- `ERC721A`：实现批量铸造;\r\n- `Merkle`树实现铸造白名单;\r\n- ... 后面有时间再更新吧，更新不易，各位大大轻喷~~\r\n\r\n参考：\r\n\r\n[EIP 721](https://learnblockchain.cn/docs/eips/eip-721.html#简要说明)\r\n\r\n[Openzeppelin-contracts--ERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.0/contracts/token/ERC721/ERC721.sol)\r\n\r\n完整项目代码见：[SolidityLongWayTODO/ERCTODO](https://github.com/XuJieJJ/SolidityLongWayTODO/tree/main/ERCTODO)","title":"以太坊开发入门(二)-深度解析ERC721标准"},"history":null,"timestamp":1724745394,"version":1}