什么是非同质化代币(NFT)?
在了解 ERC721 规范之前,我们需要弄懂 同质化代币
和 非同质化代币
的含义。
- 同质化代币(FT):可以简单类比游戏中的金币 or 点券,我拥有的 100点券和你拥有的 100 点券 本质上并没有什么区别,都可以购买皮肤。
- 非同质化代币(NFT):唯一标识具体某一个人或者物品,例如:我拥有齐白石的《群虾图》真品,全球仅此一份。
什么是 ERC721 规范?
ERC721 规范是为了实现 非同质化代币(NFT)
而创建的标准;换言之,实现了 ERC721 规范的代币,每一份都是独一无二的存在。
1. 合约结构
我们已经了解了 ERC721 规范的意义,那么我们就来看看它的结构~
如上图所示,我们可以清晰的看到 ERC721 的继承关系;从上往下来分析一下各个合约接口/抽象合约的功能:
- IERC165:要求合约提供其实现了哪些接口;在与合约进行交互的时候可以先调用此接口进行查询,了解合约具体实现了哪些接口。
- ERC165:抽象合约,官方对 IERC165 提供的默认实现。
- IERC721:发行 NFT 的标准合约规范;定义了 NFT 合约的各类行为接口,诸如:转移、授权…。
- ERC721:官方对 IERC721 提供的默认实现。
- IERC721Metadata:定义合约的元数据信息;诸如:合约名字、标志、以及每个代币的 tokenURI。
- Context:抽象合约,上下文;主要对
msg
对象做一些封装;例如:msg.Sender
代表当前调用合约的用户。 - IERC721Enumerable:提高合约的可访问性(非必须实现,但是一般我们的合约都会实现以提高可访问性);主要提供:当前发行
NFT
总量、通过索引获取用户所拥有的TokenID… - ERC721Enumerable: 抽象合约,官方对 IERC721Enumerable 提供的默认实现。
- Ownable:主要提供合约
owner
的校验以及owner
的转移;例如合约中的有些函数必须得有owner
进行调用。 - IERC721Receiver:当进行 NFT 转移的时候,如果接收的地址是一个
合约地址
的话,那么接收的合约必须实现该接口。 - Address:
Address
是solidity
特有的一种类型,项目中的libarary Address
主要是对该类型做了一些简易工具的封装。 - Strings:同上,工具类的存在,提供将
uint256
转换成string
的能力。
接下来我们就来看看合约的源码~
1.1 IERC165 接口合约
IERC165 是一个合约标准,这个标准要求合约提供其实现了哪些接口,这样在与合约进行交互的时候可以先调用此接口进行查询。
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
* 判断合约是否实现 接口ID 对应接口
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
1.2 ERC165 抽象合约
ERC165 是官方为 IERC165 提供的默认实现。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
pragma solidity ^0.8.0;
import "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
1.3 IERC721 接口合约
pragma solidity ^0.8.0;
import "./utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
// ============================事件==========================================
// 代币转移事件,当发生代币转移时触发
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
// 代币授权事件,当 owner 对代币授权于 approved 时触发
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
// 代币全量授权事件
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// ============================函数==========================================
// 查看owner账号余额(拥有的代币数)
function balanceOf(address owner) external view returns (uint256 balance);
// 获取 tokenID 对应的代币的 owner 地址
function ownerOf(uint256 tokenId) external view returns (address owner);
// 安全转换方法
function safeTransferFrom(address from, address to, uint256 tokenId) external;
// 转移方法(开发中使用上者更多,more safe..)
function transferFrom(address from, address to, uint256 tokenId) external;
// 授权,owner调用该函数,将tokenID对应的代币授予 to 账号行使权
function approve(address to, uint256 tokenId) external;
// 获取tokenID代币对应的被授权账号地址
function getApproved(uint256 tokenId) external view returns (address operator);
// 全量授权,owner调用即将自己所有的代币行使权授予 operator 账号
function setApprovalForAll(address operator, bool _approved) external;
// 判断owner是否对账号 operator 进行全量授权
function isApprovedForAll(address owner, address operator) external view returns (bool);
// 安全转移,携带回调的数据
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}
1.4 ERC721 实现合约
官方对 IERC721 接口合约提供的默认实现,我们编写的 NFT 合约一般继承于该合约;在其基础上编写自己的合约逻辑。官方实现主要提供了如下能力:
- 基础的查询能力,诸如代币的owner、合约的名字、标志…
- 代币的授权/回收、转移
- 代币的生成/销毁(注意:销毁函数无检查所属权,需要我们自行检查)
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/ERC721.sol)
pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./extensions/IERC721Metadata.sol";
import "../../utils/Address.sol";
import "../../utils/Context.sol";
import "../../utils/Strings.sol";
import "../../utils/introspection/ERC165.sol";
/**
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
* the Metadata extension, but not including the Enumerable extension, which is available separately as
* {ERC721Enumerable}.
*/
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
// Address和Strings可理解为工具类,这里是为了安全 or 操作便捷考虑。
using Address for address;
using Strings for uint256;
// 合约名字 for IERC721Metadata 中的定义
string private _name;
// 合约标志 for IERC721Metadata 中的定义
string private _symbol;
// map(tokenID, address),可理解为map结构,key为tokenID,value为该tokenID对应的owner地址
mapping(uint256 => address) private _owners;
// map(address, uint256), key为用户地址,value为该地址拥有的token数量
mapping(address => uint256) private _balances;
// map(uint256, address),key为代币ID,value为代币所授权地址(一个token只能授予一个账号行使权)
mapping(uint256 => address) private _tokenApprovals;
// Mapping from owner to operator approvals
mapping(address => mapping(address => bool)) private _operatorApprovals;
/**
* @dev 构造函数,初始化合约的 name 和 symbol
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev See {IERC165-supportsInterface}.
* 判断合约是否实现了 interfaceID 接口
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId