Cover photo

NFT 标准 ERC1155 和 ERC721 比较说明

EIP 与 ERC 概念

EIP ( Ethereum Improvement Proposals ),EIP 是一个为 Ethereum 社群提供信息的设计文件,或是用来为 Ethereum描述一些新的功能或环境,类似互联网上IETF的RFC,在设计EIP的文件里面,应提供该功能的简明技术规范,和该功能的基本原理,而EIP的作者需要自行负责文件,在社群里面的共识。

ERC全名为Ethereum Request for Comments,ERC是在Standard Track EIP里面中的其中一个项目,由于ERC所要讨论的范围是"应用程序层级的标准和协定",这个协定发布出来后有些开发者就会遵循这个标准来开发程序。开发人员可以通过提交EIP,来向以太坊社群,提出新的ERC标准提案,提交的内容包括协议规范和合同标准。 一旦EIP得到以太坊委员会的批准并最终确定后,它将成为新的ERC,新的ERC提供了一套可以为以太坊开发人员实施的标准,开发人员可以使用这些标准来构建智能合约。

简言之,EIP 是一些改进的建议,ERC 是针对 EIP 实现的标准、协议说明和代码实现,EIP 是建议文本,ERC 已经进入可执行的代码和协议层面了。

ERC721 vs ERC1155

ERC721 与 ERC1155 是 Ethereum 以及所有 EVM 兼容链生态最重要的 NFT( Non-Fungible Token )标准协议。其他与 NFT 有关的标准有 ERC721A( ERC721 扩展协议 )、 ERC2981( 版税标准协议 )、ERC4907( NFT 借贷标准协议 )。其他非 EVM 兼容链大多也参考 ERC721 和 ERC1155 定义了相关的智能合约标准,或是链上的原生 NFT 标准。链上原生 NFT 标准会有比较多的功能限制,扩展能力有限,大多数场景还是会考虑使用智能合约的实现方式。

ERC721

• Non-Fungible Token Standard,A standard interface for non-fungible tokens, also known as deeds.

• 详细说明参看 EIP 说明文档 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md

• 一个 ERC721 合约只实现一个系列的 NFT

• 一个标准的 ERC 721 NFT 通常包含

○ 名字,name

○ 缩写符号,symbol

○ tokenID,一般是自增长的 ID,每一个 NFT 都需要有一个唯一的 tokenID

○ 属性描述文件,一般是一个 JSON 文件可以存在网络文件中,也可以直接写到链上,NFT 的介绍、属性、媒体文件链接存在描述文件中

○ 媒体文件,可以存储在网络文件中,也可以直接写在链上,写入链上的数据大小有限,大多数都是存储在网络文件中,并将媒体文件链接存在描述文件中

○ 以下是一个常见的 Json 描述文件的例子

{
  "name": "Seven-step poem.",
  "description": "This is a nft generate by Come Social.",
  "external_url": "https://yourdomain.io",
  "image": "ipfs://QmTsMtab9kzXc72L8LNMvtZRp3Rfd8YLdujtsnvTTBZSMx",
  "attributes": [
    {
      "trait_type": "Author", 
      "value": "Artist"
    }, 
    {
      "trait_type": "Creation Date", 
      "value": "2022-05-01"
    }, 
    {
      "trait_type": "Work Type", 
      "value": "Ink on rice paper"
    },
    {
      "trait_type": "Origin Size", 
      "value": "68x68cm"
    },
    {
      "trait_type": "Mint Quantity", 
      "value": "1"
    }
  ]
}

• ERC721 合约中存储的数据可以理解为是一个一维的 KV 数组

• 一个常见的 ERC721 标准合约一般需要实现以下的标准接口

pragma solidity ^0.4.20;

/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
///  Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev This emits when the approved address for an NFT is changed or
    ///  reaffirmed. The zero address indicates there is no approved address.
    ///  When a Transfer event emits, this also indicates that the approved
    ///  address for that NFT (if any) is reset to none.
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev This emits when an operator is enabled or disabled for an owner.
    ///  The operator can manage all NFTs of the owner.
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice Count all NFTs assigned to an owner
    /// @dev NFTs assigned to the zero address are considered invalid, and this
    ///  function throws for queries about the zero address.
    /// @param _owner An address for whom to query the balance
    /// @return The number of NFTs owned by `_owner`, possibly zero
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT. When transfer is complete, this function
    ///  checks if `_to` is a smart contract (code size > 0). If so, it calls
    ///  `onERC721Received` on `_to` and throws if the return value is not
    ///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    /// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice Transfers the ownership of an NFT from one address to another address
    /// @dev This works identically to the other function with an extra data parameter,
    ///  except this function just sets data to "".
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
    ///  TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
    ///  THEY MAY BE PERMANENTLY LOST
    /// @dev Throws unless `msg.sender` is the current owner, an authorized
    ///  operator, or the approved address for this NFT. Throws if `_from` is
    ///  not the current owner. Throws if `_to` is the zero address. Throws if
    ///  `_tokenId` is not a valid NFT.
    /// @param _from The current owner of the NFT
    /// @param _to The new owner
    /// @param _tokenId The NFT to transfer
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice Change or reaffirm the approved address for an NFT
    /// @dev The zero address indicates there is no approved address.
    ///  Throws unless `msg.sender` is the current NFT owner, or an authorized
    ///  operator of the current owner.
    /// @param _approved The new approved NFT controller
    /// @param _tokenId The NFT to approve
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice Enable or disable approval for a third party ("operator") to manage
    ///  all of `msg.sender`'s assets
    /// @dev Emits the ApprovalForAll event. The contract MUST allow
    ///  multiple operators per owner.
    /// @param _operator Address to add to the set of authorized operators
    /// @param _approved True if the operator is approved, false to revoke approval
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice Get the approved address for a single NFT
    /// @dev Throws if `_tokenId` is not a valid NFT.
    /// @param _tokenId The NFT to find the approved address for
    /// @return The approved address for this NFT, or the zero address if there is none
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice Query if an address is an authorized operator for another address
    /// @param _owner The address that owns the NFTs
    /// @param _operator The address that acts on behalf of the owner
    /// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

• 实际的 ERC721 NFT 合约,在保证实现了标准的接口前提下,其他功能以及 NFT 的行为可以完全自定义,这就决定了 NFT 可以有非常多的自定义特性、功能或行为。NFT 不仅仅是收藏品,可以有很多的应用空间。

ERC1155

• Multi Token Standard,A standard interface for contracts that manage multiple token types. A single deployed contract may include any combination of fungible tokens, non-fungible tokens or other configurations (e.g. semi-fungible tokens).

• 详细说明参看 EIP 说明文档 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md

• 一个标准的 ERC1155 Token 通常包含

○ 名字,name

○ 缩写符号,symbol

○ Token ID,可能是唯一,也可能非唯一

○ Fungible Token,包含名字、发行量、拥有数量等信息、也可能包含媒体文件链接

○ Non-Fungible Token,包含名字、描述、属性、发行量、编号、媒体文件链接

• ERC1155 是组合型 Token 标准,一个ERC1155 Token 可以同时包含 Fungible Token 、 Non-Fungible Token、Semi-Fungible Tokens

• 一个ERC1155 合约可以同时实现多组 Non-Fungible Token

• 最早由新加坡游戏及区块链公司 Enjin 提出,后成为 Ethereum 标准 ERC

• 理论上 ERC1155 合约可以同时涵盖 ERC20 和 ERC721 的功能,但与独立的标准实现相比,灵活度有所降低,其中的 FT 的功能不完全等同于 ERC20 的代币,如不能带有小数的定义

• 对于 ERC1155 合约中的 Fungible Token 和 Semi-Fungible Tokens,可以实现批量铸造,可大量节约 Gas,但对于具备唯一性的 Non-Fungible Token,铸造的 Gas 成本取决于代码实现及写入的链上数据,与 ERC721 NFT 铸造成本基本一致。

• ERC1155 NFT 中的不同 Token,可以单独转账,单独交易

• 特别适合于游戏道具的应用场景

• 对于同时包含多个 FT 及 NFT 的ERC1155 合约,其中的 tokenID 计算逻辑需要经过重新设计,这方面没有统一的标准,EIP 网站只是提供了实现建议,这导致可能存在不同的合约实现方式,一些交易平台暂时不能很好的支持。

• 功能灵活但实现较为复杂

• ERC1155 的 Json 描述文件的形式与 ERC721 基本相同

• ERC1155 合约中的数据可以理解为是一个二维的 ID-KV 数组

• 一个标准的 ERC1155 合约需要实现的方法

pragma solidity ^0.5.9;

/**
    @title ERC-1155 Multi Token Standard
    @dev See https://eips.ethereum.org/EIPS/eip-1155
    Note: The ERC-165 identifier for this interface is 0xd9b67a26.
 */
interface ERC1155 /* is ERC165 */ {
    /**
        @dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).
        The `_operator` argument MUST be the address of an account/contract that is approved to make the transfer (SHOULD be msg.sender).
        The `_from` argument MUST be the address of the holder whose balance is decreased.
        The `_to` argument MUST be the address of the recipient whose balance is increased.
        The `_id` argument MUST be the token type being transferred.
        The `_value` argument MUST be the number of tokens the holder balance is decreased by and match what the recipient balance is increased by.
        When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).
        When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).        
    */
    event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);

    /**
        @dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).      
        The `_operator` argument MUST be the address of an account/contract that is approved to make the transfer (SHOULD be msg.sender).
        The `_from` argument MUST be the address of the holder whose balance is decreased.
        The `_to` argument MUST be the address of the recipient whose balance is increased.
        The `_ids` argument MUST be the list of tokens being transferred.
        The `_values` argument MUST be the list of number of tokens (matching the list and order of tokens specified in _ids) the holder balance is decreased by and match what the recipient balance is increased by.
        When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).
        When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).                
    */
    event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);

    /**
        @dev MUST emit when approval for a second party/operator address to manage all tokens for an owner address is enabled or disabled (absence of an event assumes disabled).        
    */
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /**
        @dev MUST emit when the URI is updated for a token ID.
        URIs are defined in RFC 3986.
        The URI MUST point to a JSON file that conforms to the "ERC-1155 Metadata URI JSON Schema".
    */
    event URI(string _value, uint256 indexed _id);

    /**
        @notice Transfers `_value` amount of an `_id` from the `_from` address to the `_to` address specified (with safety call).
        @dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).
        MUST revert if `_to` is the zero address.
        MUST revert if balance of holder for token `_id` is lower than the `_value` sent.
        MUST revert on any other error.
        MUST emit the `TransferSingle` event to reflect the balance change (see "Safe Transfer Rules" section of the standard).
        After the above conditions are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call `onERC1155Received` on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).        
        @param _from    Source address
        @param _to      Target address
        @param _id      ID of the token type
        @param _value   Transfer amount
        @param _data    Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to`
    */
    function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

    /**
        @notice Transfers `_values` amount(s) of `_ids` from the `_from` address to the `_to` address specified (with safety call).
        @dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).
        MUST revert if `_to` is the zero address.
        MUST revert if length of `_ids` is not the same as length of `_values`.
        MUST revert if any of the balance(s) of the holder(s) for token(s) in `_ids` is lower than the respective amount(s) in `_values` sent to the recipient.
        MUST revert on any other error.        
        MUST emit `TransferSingle` or `TransferBatch` event(s) such that all the balance changes are reflected (see "Safe Transfer Rules" section of the standard).
        Balance changes and events MUST follow the ordering of the arrays (_ids[0]/_values[0] before _ids[1]/_values[1], etc).
        After the above conditions for the transfer(s) in the batch are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call the relevant `ERC1155TokenReceiver` hook(s) on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).                      
        @param _from    Source address
        @param _to      Target address
        @param _ids     IDs of each token type (order and length must match _values array)
        @param _values  Transfer amounts per token type (order and length must match _ids array)
        @param _data    Additional data with no specified format, MUST be sent unaltered in call to the `ERC1155TokenReceiver` hook(s) on `_to`
    */
    function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;

    /**
        @notice Get the balance of an account's tokens.
        @param _owner  The address of the token holder
        @param _id     ID of the token
        @return        The _owner's balance of the token type requested
     */
    function balanceOf(address _owner, uint256 _id) external view returns (uint256);

    /**
        @notice Get the balance of multiple account/token pairs
        @param _owners The addresses of the token holders
        @param _ids    ID of the tokens
        @return        The _owner's balance of the token types requested (i.e. balance for each (owner, id) pair)
     */
    function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);

    /**
        @notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens.
        @dev MUST emit the ApprovalForAll event on success.
        @param _operator  Address to add to the set of authorized operators
        @param _approved  True if the operator is approved, false to revoke approval
    */
    function setApprovalForAll(address _operator, bool _approved) external;

    /**
        @notice Queries the approval status of an operator for a given owner.
        @param _owner     The owner of the tokens
        @param _operator  Address of authorized operator
        @return           True if the operator is approved, false if not
    */
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

• 一个简易的 ERC1155 的合约例子

// contracts/GameItems.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

contract GameItems is ERC1155 {
    uint256 public constant GOLD = 0;
    uint256 public constant SILVER = 1;
    uint256 public constant THORS_HAMMER = 2;
    uint256 public constant SWORD = 3;
    uint256 public constant SHIELD = 4;

    constructor() public ERC1155("https://game.example/api/item/{id}.json") {
        _mint(msg.sender, GOLD, 10**18, "");
        _mint(msg.sender, SILVER, 10**27, "");
        _mint(msg.sender, THORS_HAMMER, 1, "");
        _mint(msg.sender, SWORD, 10**9, "");
        _mint(msg.sender, SHIELD, 10**9, "");
    }
}

• 关于例子的说明

○ 这是一个游戏道具常规使用场景

○ 代码继承了 ERC1155 合约,其他的标准函数在继承的 ERC1155 中定义

○ 其中的道具从 GOLD 开始,定义了从 0 开始的 tokenID,也可以写成一个枚举类型定义

○ 构造函数初始化铸造了各类 Token,其中的 THORS_HAMMER 是唯一的,相当于一个 NFT,其他类型相当于是 Semi-FT

○ 可以在代码中增加铸造其他类型 Token 的功能,可以是增加新的类型,也可以是增加某一个已有类型的数量

○ 构造函数传入了描述文件的链接,{id} 是一个规范写法,要求读取的客户端能够自动将 {id} 替换成 tokenID

○ 如果每一个 ID 对应的铸造数量都是1 ,每一个都是 NFT

○ 如果要更灵活一些,需要定义复杂的 ID,并且要在合约中编写 ID 的生成和读取计算

○ 这是最基本的例子,其他功能都需要再定义,比较丰富的例子可以参考 Opensea 的一个例子实现,代码库中mock 目录下就是可部署的测试合约 https://github.com/ProjectOpenSea/multi-token-standard

两种合约各有优缺点,总体来说 ERC1155 可以涵盖了 ERC721 的功能,并且有更灵活的功能和更高的扩展性,但合约的实现复杂度会高出不少,并且目前 NFT 交易平台和钱包 APP 对 ERC1155 NFT 的支持还不够友好。选择哪一种标准作为 NFT 项目的开发标准,还是要根据实际应用场景来定。