# NFT 标准 ERC1155 和 ERC721 比较说明

By [MrBWG](https://paragraph.com/@mrbwg) · 2023-03-16

---

**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](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](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](https://github.com/ProjectOpenSea/multi-token-standard)

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

---

*Originally published on [MrBWG](https://paragraph.com/@mrbwg/nft-erc1155-erc721)*
