NFT 这么火,你还不懂 ERC721 吗?

什么是非同质化代币(NFT)?

在了解 ERC721 规范之前,我们需要弄懂 同质化代币非同质化代币 的含义。

  • 同质化代币(FT):可以简单类比游戏中的金币 or 点券,我拥有的 100点券和你拥有的 100 点券 本质上并没有什么区别,都可以购买皮肤。
  • 非同质化代币(NFT):唯一标识具体某一个人或者物品,例如:我拥有齐白石的《群虾图》真品,全球仅此一份。

什么是 ERC721 规范?

ERC721 规范是为了实现 非同质化代币(NFT) 而创建的标准;换言之,实现了 ERC721 规范的代币,每一份都是独一无二的存在。

1. 合约结构

我们已经了解了 ERC721 规范的意义,那么我们就来看看它的结构~

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:Addresssolidity 特有的一种类型,项目中的 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 ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC721-balanceOf}.
     * 返回账户拥有的代币数
     */
    function balanceOf(address owner) public view virtual override returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");
        return _balances[owner];
    }

    /**
     * @dev See {IERC721-ownerOf}.
     * 返回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;
    }

    /**
     * @dev See {IERC721Metadata-name}.
     * 元数据: 返回合约名字
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev See {IERC721Metadata-symbol}.
     * 元数据:返回合约标志
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev See {IERC721Metadata-tokenURI}.
     * 元数据:返回tokenID代币对应的 tokenURI;tokenURI在后文会详解介绍。
     */
    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())) : "";
    }

    /**
     * @dev Base URI for computing {tokenURI}.
     * tokenURI 一般构建方式是 : baseURI + tokenID + baseExt的方式
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }

    /**
     * @dev See {IERC721-approve}.
     * 调用者msgSender()对账号to授予tokenID的行使权
     */
    function approve(address to, uint256 tokenId) public virtual override {
        address owner = ERC721.ownerOf(tokenId);
        // 要求:被授权账号to不能为代币的owner
        require(to != owner, "ERC721: approval to current owner");
				// 要求:函数调用者为代币的owner或者函数调用者是owner设置的所有代币授权用户
        require(
            _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
            "ERC721: approve caller is not owner nor approved for all"
        );
				// 内部函数,执行真正的授权操作
        _approve(to, tokenId);
    }
    
     /**
     * @dev Approve `to` to operate on `tokenId`
     * Emits a {Approval} event.
     * 授予to账号tokenID的行使权,并触发授权事件
     */
    function _approve(address to, uint256 tokenId) internal virtual {
        _tokenApprovals[tokenId] = to;
        emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
    }

    /**
     * @dev See {IERC721-getApproved}.
     * 获取代币tokenID的被授权用户地址
     */
    function getApproved(uint256 tokenId) public view virtual override returns (address) {
    		// 参数判断:tokenID必须存在
        require(_exists(tokenId), "ERC721: approved query for nonexistent token");
        return _tokenApprovals[tokenId];
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     * 函数调用者msgSender()将自己所有的代币的行使权授予operator账号 (授予/回收)
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
    		// 内部函数,执行真正的授权/回收操作
        _setApprovalForAll(_msgSender(), operator, approved);
    }
		
		/**
     * @dev Approve `operator` to operate on all of `owner` tokens
     * Emits a {ApprovalForAll} event.
     * owner授予/回收operator对自己所有代币的权限 
     * 触发 ApprovalForAll 事件
     */
    function _setApprovalForAll(
        address owner,
        address operator,
        bool approved
    ) internal virtual {
    		// 参数检查:owner不能为operator(被授权对象)
        require(owner != operator, "ERC721: approve to caller");
        // 全局变量记录授权/回收结果
        _operatorApprovals[owner][operator] = approved;
        // 触发事件
        emit ApprovalForAll(owner, operator, approved);
    }
    
    /**
     * @dev See {IERC721-isApprovedForAll}.
     * 判断用户owner是否对operator进行所有代币授权
     */
    function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev See {IERC721-transferFrom}.
     * 代币转移,将tokenID从from账号转移到to账号。
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        // 前置判断:是代币的owner或者拥有行使权的用户
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        // 内部函数,执行真实的代币转移。
        _transfer(from, to, tokenId);
    }
    
    /**
     * @dev Returns whether `spender` is allowed to manage `tokenId`.
     * Requirements:
     * - `tokenId` must exist.
     * 内部方法:判断地址是否为代币的owner或者是拥有代币行使权的账号
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
    		// 前置判断:tokenID必须存在
        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
        address owner = ERC721.ownerOf(tokenId);
        // owner? or 授权用户? or 全量授权用户?
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }
    
     /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *  As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
     * Requirements:
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * Emits a {Transfer} event.
     * 内部真实执行代币转移的函数
     */
    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {
    		// 前置检查:被转移的代币属于from用户
        require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
        // 前置检查:接收代币的用户to不能为零地址
        require(to != address(0), "ERC721: transfer to the zero address");
				// 钩子函数: 代币转移前触发,官方实现无做实际操作,如果业务有特殊的逻辑,我理解可重写 _beforeTokenTransfer函数
        _beforeTokenTransfer(from, to, tokenId);

        // 清除之前的授权—————指向零地址
        _approve(address(0), tokenId);
				// from账号代币数 -1
        _balances[from] -= 1;
        // to账号代币数 +1
        _balances[to] += 1;
        // 执行代币的转移,即更换owner地址
        _owners[tokenId] = to;
				// 触发转移事件
        emit Transfer(from, to, tokenId);
				// 钩子函数:代币转移后触发,官方实现无做实际操作
        _afterTokenTransfer(from, to, tokenId);
    }
		
		
    /**
     * @dev See {IERC721-safeTransferFrom}.
     * 安全代币转移,真实中使用该函数比较合适。
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     * 安全转移函数,携带回调数据 _data
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) public virtual override {
    		// 前置检查:函数调用者是tokenID的owner或者拥有操作权限
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
        // 内部函数,执行真正的安全转移
        _safeTransfer(from, to, tokenId, _data);
    }

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     * `_data` is additional data, it has no specified format and it is sent in call to `to`.
     * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
     * implement alternative mechanisms to perform token transfer, such as signature-based.
     *
     * Requirements:
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeTransfer(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
    		// 内部函数:执行转移
        _transfer(from, to, tokenId);
        // 安全的由来:代币接收者如果是一个合约,那么其必须要实现IERC721Receiver规范,否则转移失败
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev Returns whether `tokenId` exists.
     * 内部函数,判断代币是否存在
     */
    function _exists(uint256 tokenId) internal view virtual returns (bool) {
        return _owners[tokenId] != address(0);
    }

    /**
     * @dev Safely mints `tokenId` and transfers it to `to`.
     * Requirements:
     * - `tokenId` must not exist.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     * Emits a {Transfer} event.
     * 内部函数:安全铸造NFT,我们编写的合约在铸造NFT的时候,调用的就是该方法。
     */
    function _safeMint(address to, uint256 tokenId) internal virtual {
    		// 内部函数:铸造
        _safeMint(to, tokenId, "");
    }

    /**
     * 真实铸造NFT函数,内部函数
     */
    function _safeMint(
        address to,
        uint256 tokenId,
        bytes memory _data
    ) internal virtual {
    		// 铸造
        _mint(to, tokenId);
        // 判断接收者是否是合约,如果是合约则需要实现 ERC721Receiver 规范。
        require(
            _checkOnERC721Received(address(0), to, tokenId, _data),
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }

    /**
     * @dev Mints `tokenId` and transfers it to `to`.
     * Requirements:
     * - `tokenId` must not exist.
     * - `to` cannot be the zero address.
     * Emits a {Transfer} event.
     * 内部函数:铸造代币,但是我们一般不直接调用该函数;调用 _safeMint 更安全
     */
    function _mint(address to, uint256 tokenId) internal virtual {
    		// 前置检查:代币接收者不能是零地址
        require(to != address(0), "ERC721: mint to the zero address");
        // 前置检查:代币的tokenID不能是已存在的(独一无二)
        require(!_exists(tokenId), "ERC721: token already minted");
				// 钩子函数:代币转移前触发
        _beforeTokenTransfer(address(0), to, tokenId);
				// to账号代币数+1
        _balances[to] += 1;
        // 代币归属
        _owners[tokenId] = to;
				// 触发转移实现,从零地址发起,说明是铸造生成
        emit Transfer(address(0), to, tokenId);
				// 钩子函数:代币转移后触发
        _afterTokenTransfer(address(0), to, tokenId);
    }

    /**
     * @dev Destroys `tokenId`.
     * The approval is cleared when the token is burned.
     * Requirements:
     * - `tokenId` must exist.
     * Emits a {Transfer} event.
     * 内部函数:代币销毁
     * 注意:函数并没有做权限检查。所以我们在我们的合约中如果要调用该函数,需要自己做tokenID的所有权检查!
     */
    function _burn(uint256 tokenId) internal virtual {
        address owner = ERC721.ownerOf(tokenId);
				// 钩子函数:代币转移前触发
        _beforeTokenTransfer(owner, address(0), tokenId);

				// 清除授权
        _approve(address(0), tokenId);
				// 用户代币数-1
        _balances[owner] -= 1;
        // 清理代币
        delete _owners[tokenId];
				// 触发代币转移事件,代币的接受者为零地址,说明是销毁
        emit Transfer(owner, address(0), tokenId);
				// 钩子函数:代币转移后触发
        _afterTokenTransfer(owner, address(0), tokenId);
    }


    /**
     * 内部方法:判断代币接收者是否是合约,并且是否实现了 IERC721Receiver 接口
     * 我们上面有介绍,如果代币的接受者是一个合约的话,那么合约需要实现 IERC721Receiver 接口
     */
    function _checkOnERC721Received(
        address from,
        address to,
        uint256 tokenId,
        bytes memory _data
    ) private returns (bool) {
    		// 接收者是合约
        if (to.isContract()) {
        		// 判断是否实现了 IERC721Receiver
            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;
        }
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     * Calling conditions:
     * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
     * transferred to `to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` and `to` are never both zero.
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     * 钩子函数 
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {}

    /**
     * @dev Hook that is called after any transfer of tokens. This includes
     * minting and burning.
     * Calling conditions:
     * - when `from` and `to` are both non-zero.
     * - `from` and `to` are never both zero.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     * 钩子函数
     */
    function _afterTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual {}
}
复制代码

1.5 IERC721Metadata 接口合约

定义合约的元数据信息;诸如:合约名字、标志、以及每个代币的 tokenURI。那么 tokenURI 是什么呢?

TokenURI 是 ERC721 规范提出用以描述 NFT 资源;我们可以通过 NFT 的 TokenURI 获取到该 NFT 对应的描述资源。假如有一款宠物养成类游戏,在对宠物数据进行上链的时候并不会完整的将整个宠物信息进行上链。一方面是成本问题,链上存储的数据越多,所需要的成本就越高;另外一方面则是诸如宠物等级等信息会随着宠物的成长而不断的变化,如果上链则会频繁的修改该数据,也不合适。因此 TokenURI 的存在就是为了解决这类问题,它是对 NFT 资源的扩充元描述

那比如退一步说:我的上链元数据不存在更新的场景并且数据量不大;那还存在维护 TokenURI 的必要吗?其实上,将所有的元数据存储在链上是正确的方式,但是目前很多 NFT 市场(例如OpenSea)不知道如何读取链上元数据(或者说是不支持);所以目前来说,使用 TokenURI 的链下元数据来可视化我们的 NFT,同时拥有所有的链上元数据是最理想的,这样你的代币就可以互相交互。

那如何维护 TokenURI 呢?比较常见的方式有 2种。

中心化存储,我们可以采用中心化存储系统,将 tokenURI 维护到我们自身的系统中;优点是操作简单,缺点也比较明显;当中心化系统故障后则无法响应。

IPFS 是一种点对点的超媒体协议,旨在使网络更快、更安全、更开放。它允许任何人上传一个文件,并且该文件是经过哈希校验的,所以如果文件发生改变,它的哈希值也会改变。这对于存储图片来说是非常理想的,因为这意味着每次图片更新时,链上的哈希/tokenURI也要改变,同时这意味着我们可以拥有元数据的历史记录。将图像添加到 IPFS 上也非常简单,而且不需要运行服务器。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Metadata is IERC721 {
    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);
}
复制代码

1.6 IERC721Enumerable 接口合约

提高合约的可访问性(非必须实现,但是一般我们的合约都会实现以提高可访问性);主要提供:当前发行 NFT 总量、通过索引获取用户所拥有的TokenID…

pragma solidity ^0.8.0;
import "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     * 返回NFT总量
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     * 所有者可以一次拥有多个的NFT, 此函数返回_owner拥有的NFT列表中对应索引的tokenId
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     * 通过索引返回对应的tokenId
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}
复制代码

1.7 ERC721Enumerable 抽象合约

官方对 IERC721Enumerable 提供的默认实现。代码比较简单,不做过度解析,自行阅读~

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/ERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "./IERC721Enumerable.sol";

/**
 * @dev This implements an optional extension of {ERC721} defined in the EIP that adds
 * enumerability of all the token ids in the contract as well as all token ids owned by each
 * account.
 */
abstract contract ERC721Enumerable is ERC721, IERC721Enumerable {
    // Mapping from owner to list of owned token IDs
    mapping(address => mapping(uint256 => uint256)) private _ownedTokens;

    // Mapping from token ID to index of the owner tokens list
    mapping(uint256 => uint256) private _ownedTokensIndex;

    // Array with all token ids, used for enumeration
    uint256[] private _allTokens;

    // Mapping from token id to position in the allTokens array
    mapping(uint256 => uint256) private _allTokensIndex;

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
        return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds");
        return _ownedTokens[owner][index];
    }

    /**
     * @dev See {IERC721Enumerable-totalSupply}.
     */
    function totalSupply() public view virtual override returns (uint256) {
        return _allTokens.length;
    }

    /**
     * @dev See {IERC721Enumerable-tokenByIndex}.
     */
    function tokenByIndex(uint256 index) public view virtual override returns (uint256) {
        require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds");
        return _allTokens[index];
    }

    /**
     * @dev Hook that is called before any token transfer. This includes minting
     * and burning.
     *
     * Calling conditions:
     *
     * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
     * transferred to `to`.
     * - When `from` is zero, `tokenId` will be minted for `to`.
     * - When `to` is zero, ``from``'s `tokenId` will be burned.
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     *
     * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, tokenId);

        if (from == address(0)) {
            _addTokenToAllTokensEnumeration(tokenId);
        } else if (from != to) {
            _removeTokenFromOwnerEnumeration(from, tokenId);
        }
        if (to == address(0)) {
            _removeTokenFromAllTokensEnumeration(tokenId);
        } else if (to != from) {
            _addTokenToOwnerEnumeration(to, tokenId);
        }
    }

    /**
     * @dev Private function to add a token to this extension's ownership-tracking data structures.
     * @param to address representing the new owner of the given token ID
     * @param tokenId uint256 ID of the token to be added to the tokens list of the given address
     */
    function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private {
        uint256 length = ERC721.balanceOf(to);
        _ownedTokens[to][length] = tokenId;
        _ownedTokensIndex[tokenId] = length;
    }

    /**
     * @dev Private function to add a token to this extension's token tracking data structures.
     * @param tokenId uint256 ID of the token to be added to the tokens list
     */
    function _addTokenToAllTokensEnumeration(uint256 tokenId) private {
        _allTokensIndex[tokenId] = _allTokens.length;
        _allTokens.push(tokenId);
    }

    /**
     * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that
     * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for
     * gas optimizations e.g. when performing a transfer operation (avoiding double writes).
     * This has O(1) time complexity, but alters the order of the _ownedTokens array.
     * @param from address representing the previous owner of the given token ID
     * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address
     */
    function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private {
        // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = ERC721.balanceOf(from) - 1;
        uint256 tokenIndex = _ownedTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary
        if (tokenIndex != lastTokenIndex) {
            uint256 lastTokenId = _ownedTokens[from][lastTokenIndex];

            _ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
            _ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index
        }

        // This also deletes the contents at the last position of the array
        delete _ownedTokensIndex[tokenId];
        delete _ownedTokens[from][lastTokenIndex];
    }

    /**
     * @dev Private function to remove a token from this extension's token tracking data structures.
     * This has O(1) time complexity, but alters the order of the _allTokens array.
     * @param tokenId uint256 ID of the token to be removed from the tokens list
     */
    function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private {
        // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and
        // then delete the last slot (swap and pop).

        uint256 lastTokenIndex = _allTokens.length - 1;
        uint256 tokenIndex = _allTokensIndex[tokenId];

        // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so
        // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding
        // an 'if' statement (like in _removeTokenFromOwnerEnumeration)
        uint256 lastTokenId = _allTokens[lastTokenIndex];

        _allTokens[tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token
        _allTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index

        // This also deletes the contents at the last position of the array
        delete _allTokensIndex[tokenId];
        _allTokens.pop();
    }
}
复制代码

1.8 IERC721Receiver 接口合约

上面讲到在做 NFT 的转移时,如果 _to 是一个合约的话,那么其必须实现 IERC721Receiver接口才可以接收。

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.3.2 (token/ERC721/IERC721Receiver.sol)
​
pragma solidity ^0.8.0;
​
/**
 * @title ERC721 token receiver interface
 * @dev Interface for any contract that wants to support safeTransfers
 * from ERC721 asset contracts.
 */
interface IERC721Receiver {
    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
     *
     * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}
复制代码

2. 工具合约

在合约结构中,还有一些诸如 Context、Ownerable 等工具合约,代码都比较简单,这里就不做过多的解析。

3. 总结

本文主要对 ERC721 规范进行了解析,有了 OpenZeeppline 提供的默认实现;我们在编写自己的合约时其实所需要做的事情并不多。有几个比较核心的点:

  1. 确认上链数据。
  2. TokenURI 的维护。
  3. 合理的 Mint 和 Burn:TokenID 由于要求唯一性,可采用自增的方式来维护;内部提供的 _burn 函数并没有做权限校验,需要我们外层保证。
  4. 事件的处理,事件是连接去中心化系统和中心化系统的桥梁;我们对链上的事件进行监听,捕获到即使不做任何操作也可以做日志流水记录到 DB 中。

后台开发的童鞋可能会有疑问(一开始我也有):在诸如 _transfer 函数中做了多个操作,并且没有做事务保证;是否会存在问题呢?

链上在执行的时候是串行化的,无并发问题;并且在执行函数时,如果发生了错误;之前所有的执行都会回滚。因而我们可以不用考虑并发和事务等问题。

另:本文是从技术角度进行分析,我们应严格遵守国家法律法规;不做任何违法犯罪的事情!

笔芯~

おすすめ

転載: juejin.im/post/7074435484918743076