# NFT 标准 ERC1155 和 ERC721 比较说明 **Published by:** [MrBWG](https://paragraph.com/@mrbwg/) **Published on:** 2023-03-16 **URL:** https://paragraph.com/@mrbwg/nft-erc1155-erc721 ## Content 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 项目的开发标准,还是要根据实际应用场景来定。 ## Publication Information - [MrBWG](https://paragraph.com/@mrbwg/): Publication homepage - [All Posts](https://paragraph.com/@mrbwg/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@mrbwg): Subscribe to updates