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 项目的开发标准,还是要根据实际应用场景来定。