介绍

什么是非同质化代币?
非同质化代币(NFT)用于以唯一的方式标识某人或者某物。 此类型的代币可以被完美地用于出售下列物品的平台:收藏品、密钥、彩票、音乐会座位编号、体育比赛等。 这种类型的代币有着惊人的潜力,因此它需要一个适当的标准。ERC-721 就是为解决这个问题而来!

ERC-721 是什么?

和ERC20一样,ERC721同样是一个代币标准,ERC721官方简要解释是Non-Fungible Tokens,简写为NFTs。
那怎么理解非同质代币呢?ERC20代币是可置换的,且可细分为N份(1 = 10 * 0.1), 而ERC721的Token最小的单位为1,无法再分割。所以每一个erc721的token都是独一无二的。

ERC721标准

ERC721作为一个合约标准,提供了在实现ERC721代币时必须要遵守的协议,要求每个ERC721标准合约需要实现ERC721及ERC165接口,接口定义如下:


interface ERC721 /* is ERC165 */ {
    
    event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    //返回由_owner持有的NFTs的数量
    function balanceOf(address _owner) external view returns (uint256);
    //返回tokenid代币持有者的地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    //转移NFT所有权,一次成功的转移操作必须发起 Transer 事件
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    //transferFrom(): 用来转移NFTs, 方法成功后需触发Transfer事件。
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    //授予地址_approved具有_tokenId的控制权,方法成功后需触发Approval 事件
    function approve(address _approved, uint256 _tokenId) external payable;
    //授予地址_operator具有所有NFTs的控制权,成功后需触发ApprovalForAll事件。
    function setApprovalForAll(address _operator, bool _approved) external;
    //用来查询某个tokenid的授权。
    function getApproved(uint256 _tokenId) external view returns (address);
    //查询_owner 对 _operator是否所有授权
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

其中我们需要特别注意 safeTransferFrom(): 的用法:
safeTransferFrom(): 转移NFT所有权,一次成功的转移操作必须发起 Transer 事件。函数的实现需要做一下几种检查:
1.调用者msg.sender应该是当前tokenId的所有者或被授权的地址
2._from 必须是 _tokenId的所有者
3._tokenId 应该是当前合约正在监测的NFTs 中的任何一个
4._to 地址不应该为 0
5.如果_to 是一个合约应该调用其onERC721Received方法, 并且检查其返回值,如果返回值不为bytes4(keccak256("onERC721Received(address,uint256,bytes)"))抛出异常。
一个可接收NFT的合约必须实现ERC721TokenReceiver接口:

        /// @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
        function onERC721Received(address _from, uint256 _tokenId, bytes data) external returns(bytes4);
    }

transferFrom(): 用来转移NFTs, 方法成功后需触发Transfer事件。调用者自己确认_to地址能正常接收NFT,否则将丢失此NFT。此函数实现时需要检查上面条件的前4条。

ERC165 标准

ERC721标准同时要求必须符合ERC165标准 ,其接口如下:

    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

ERC165同样是一个合约标准,这个标准要求合约提供其实现了哪些接口,这样再与合约进行交互的时候可以先调用此接口进行查询。
interfaceID为函数选择器,计算方式有两种,如:bytes4(keccak256('supportsInterface(bytes4)'));或ERC165.supportsInterface.selector,多个函数的接口ID为函数选择器的异或值。

可选实现接口:ERC721Metadata

ERC721Metadata 接口用于提供合约的元数据:name , symbol 及 URI(NFT所对应的资源)。
其接口定义如下:

    function name() external pure returns (string _name);
    function symbol() external pure returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string);
}

接口说明:
1.name(): 返回合约名字,尽管是可选,但强烈建议实现,即便是返回空字符串。
2.symbol(): 返回合约代币符号,尽管是可选,但强烈建议实现,即便是返回空字符串。
3.tokenURI(): 返回_tokenId所对应的外部资源文件的URI(通常是IPFS或HTTP(S)路径)。外部资源文件需要包含名字、描述、图片,其格式的要求如下:

    "title": "Asset Metadata",
permalink: token-erc721
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents",
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents",
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive.",
        }
    }
}

tokenURI通常是被web3调用,以便在应用层做相应的查询和展示。

可选实现接口:ERC721Enumerable

ERC721Enumerable的主要目的是提高合约中NTF的可访问性,其接口定义如下:

    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 _index) external view returns (uint256);
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

接口说明:
1.totalSupply(): 返回NFT总量
2.tokenByIndex(): 通过索引返回对应的tokenId。
3.tokenOfOwnerByIndex(): 所有者可以一次拥有多个的NFT, 此函数返回_owner拥有的NFT列表中对应索引的tokenId。

补充说明

NTF IDs

NTF ID,即tokenId,在合约中用唯一的uint265进行标识,每个NFT的ID在智能合约的生命周期内不允许改变。推荐的实现方式有:
1.从0开始,每新加一个NFT,NTF ID加1
2.使用sha3后uuid 转换为 NTF ID

与ERC-20的兼容性

ERC721标准尽可能遵循 ERC-20 的语义,但由于同质代币与非同质代币之间的根本差异,并不能完全兼容ERC-20。

交易、挖矿、销毁

在实现transter相关接口时除了满足上面的的条件外,我们可以根据需要添加自己的逻辑,如加入黑名单等。同时挖矿、销毁尽管不是标准的一部分,我们可以根据需要实现。

openzeppelin 中 ERC-721的实现

在@openzeppelin/constracts/erc721.sol中实现了erc721的标准。
ERC721合约继承了 Context, 抽象合约,ERC165, IERC721, IERC721Metadata接口合约。
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
1.Context 抽象合约提供了有关当前执行上下文的信息,包括事务的发送者及其数据。可以调用获得msg.sender 地址。
2.ERC165 提供了supportsInterface 接口。可以通过type(IERC721).interfaceId来查询合约实现了哪些接口。
3.IERC721,erc721的标准接口。
4.IERC721Metadata提供了合约的元数据接口,包括nfttoken的name,symbol,tokenUrl。
并且相对于ERC20 _mint接口需要提供 tokenid 并且增加了_setTokenURI接口将tokenID和tokenUrl绑定。tokenUrl通常为IPFS地址或者HTTPS地址。

构建 ERC721 代币合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract GameItem is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() public ERC721("GameItem", "ITM") {}

    function awardItem(address player, string memory tokenURI)
        public
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

ERC721合同包括所有标准扩展(和IERC721Metadata)IERC721Enumerable。这就是该_setTokenURI方法的来源:我们使用它来存储项目的元数据。
另请注意,与 ERC20 不同,ERC721 缺少decimals字段,因为每个令牌都是不同的并且不能被分区。


> gameItem.awardItem(playerAddress, "https://game.example/item-id-8u5h2m.json")
Transaction successful. Transaction hash: 0x...
Events emitted:
 - Transfer(0x0000000000000000000000000000000000000000, playerAddress, 7)
以及查询到的每个项目的所有者和元数据:

> gameItem.ownerOf(7)
playerAddress
> gameItem.tokenURI(7)
"https://game.example/item-id-8u5h2m.json"
这tokenURI应该解析为可能类似于以下内容的 JSON 文档:

{
    "name": "Thor's hammer",
    "description": "Mjölnir, the legendary hammer of the Norse god of thunder.",
    "image": "https://game.example/item-id-8u5h2m.png",
    "strength": 20
}

您会注意到该项目的信息包含在元数据中,但该信息不在链上!所以游戏开发者可以改变底层元数据,改变游戏规则!如果你想把所有的物品信息放在链上,你可以扩展 ERC721 来这样做(虽然它会相当昂贵)。您还可以利用 IPFS 来存储 tokenURI 信息。