Description
This proposal has been accepted and merged as a draft standard, please see the officially tracked version for the current draft.
Please see PR #841 for the discussions leading up to this draft, and use this thread (#721) for further discussion. (Or, if you have a concrete proposal, consider opening a new PR with your proposed changes.)
Original Draft (Sep 20, 2017)
Preamble
EIP: <to be assigned>
Title: Non-fungible Token Standard
Author: Dieter Shirley <dete@axiomzen.co>
Type: Standard
Category: ERC
Status: Draft
Created: 2017-09-20
Simple Summary
A standard interface for non-fungible tokens.
Abstract
The following standard allows for the implementation of a standard API for non-fungible tokens (henceforth referred to as "NFTs") within smart contracts. This standard provides basic functionality to track and transfer ownership of NFTs.
Motivation
A standard interface allows any NFTs on Ethereum to be handled by general-purpose applications. In particular, it will allow for NFTs to be tracked in standardized wallets and traded on exchanges.
Specification
I wanted to get the community's "first impression" before spending a bunch of time detailing out these end-points; expect this section to be significantly expanded after the first round of feedback. I've left out "obvious" return values for skimmability, and included a few notes where the functionality warrants special interest.
- ERC-20 compatibility:
name()
optionalsymbol()
optionaltotalSupply()
- Number of NFTs tracked by this contractbalanceOf(address _owner)
- Number of NFTs owned by a particular address
- Basic ownership:
tokensOfOwnerByIndex(address _owner, uint _index) constant returns (uint tokenId)
- There's really no good way to return a list of NFTs by owner, but it's valuable functionality. You should strenuously avoid calling this method "on-chain" (i.e. from a non-constant
contract function).ownerOf(uint _tokenId) constant returns (address owner)
transfer(address _to, uint _tokenId)
approve(address _to, uint _tokenId)
– SHOULD be cleared by anytransfer()
operationtransferFrom(address _from, address _to, unit _tokenId)
- the sender must have been previously authorized byapprove()
. (Note: Technically, the_from
address here can be inferred by callingownerOf(_tokenId)
. I've left it in for symmetry with the corresponding ERC-20 method, and to forestall the (somewhat subtle) bug that could result from not clearing theapprove
authorization inside a successfultransfer
call.)
- NFT metadata (optional):
tokenMetadata(uint _tokenId) returns (string infoUrl)
- recommended format is IPFS or HTTP multiaddress withname
,image
, anddescription
sub-paths. IPFS is the preferred mechanism (immutable and more durable). Example: IftokenMetadata()
returns/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG
, the object description would be accessible viaipfs cat /ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/description
.
Rationale
There are many proposed uses of Ethereum smart contracts that depend on tracking individual, non-fungible tokens (NFTs). Examples of existing or planned NFTs are LAND in Decentraland, the eponymous punks in CryptoPunks, and in-game items using systems like Dmarket or EnjinCoin. Future uses include tracking real-world non-fungible assets, like real-estate (as envisioned by companies like Ubitquity or Propy). It is critical in each of these cases that these items are not "lumped together" as numbers in a ledger, but instead each token must have its ownership individually and atomically tracked. Regardless of the nature of these items, the ecosystem will be stronger if we create a standardized interface that allows for cross-functional non-fungible token management and sales platforms.
The basis of this standard is that every NFT is identified by a unique, 256-bit unsigned integer within its tracking contract. The pair (contract address, asset ID)
will then be globally unique within the Ethereum ecosystem.
This standard has followed the model of ERC-20 as much as possible to minimize the effort required for wallets (in particular) to track non-fungible tokens, while echoing a well-understood standard.
Backwards Compatibility
This standard follows the semantics of ERC-20 as closely as possible, but can't be entirely compatible with it due to the fundamental differences between fungible and non-fungible tokens.
Example non-fungible implementations as of September, 2017:
- CryptoPunks - Partially ERC-20 compatible, but not easily generalizable because it includes auction functionality directly in the contract and uses function names that explicitly refer to the NFTs as "punks".
- Auctionhouse Asset Interface - @dob needed a generic interface for his Auctionhouse dapp (currently ice-boxed). His "Asset" contract is very simple, but is missing ERC-20 compatiblity, approve() functionality, and metadata. This effort is referenced in the discussion for EIP-173.
(It should be noted that "limited edition, collectable tokens" like Curio Cards and Rare Pepe are not non-fungible tokens. They're actually a collection of individual fungible tokens, each of which is tracked by its own smart contract with its own total supply (which may be 1
in extreme cases).)
Implementation
Reference implementation forthcoming...
Copyright
Copyright and related rights waived via CC0.
Second Draft (Nov 9, 2017)
Preamble
EIP: <to be assigned>
Title: Non-fungible Token Standard
Author: Dieter Shirley <dete@axiomzen.co>
Type: Standard
Category: ERC
Status: Draft
Created: 2017-09-20
Simple Summary
A standard interface for non-fungible tokens.
Abstract
This standard allows for the implementation of a standard API for non-fungible tokens (henceforth referred to as "NFTs") within smart contracts. This standard provides basic functionality to track and transfer ownership of NFTs.
Motivation
A standard interface allows any NFTs on Ethereum to be handled by general-purpose applications. In particular, it will allow for NFTs to be tracked in standardized wallets and traded on exchanges.
Specification
ERC-20 Compatibility
name
function name() constant returns (string name)
OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.
Returns the name of the collection of NFTs managed by this contract. - e.g. "My Non-Fungibles"
.
symbol
function symbol() constant returns (string symbol)
OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.
Returns a short string symbol referencing the entire collection of NFTs managed in this contract. e.g. "MNFT". This symbol SHOULD be short (3-8 characters is recommended), with no whitespace characters or new-lines and SHOULD be limited to the uppercase latin alphabet (i.e. the 26 letters used in English).
totalSupply
function totalSupply() constant returns (uint256 totalSupply)
Returns the total number of NFTs currently tracked by this contract.
balanceOf
function balanceOf(address _owner) constant returns (uint256 balance)
Returns the number of NFTs assigned to address _owner
.
Basic Ownership
ownerOf
function ownerOf(uint256 _tokenId) constant returns (address owner)
Returns the address currently marked as the owner of _tokenID
. This method MUST throw
if _tokenID
does not represent an NFT currently tracked by this contract. This method MUST NOT return 0 (NFTs assigned to the zero address are considered destroyed, and queries about them should throw
).
approve
function approve(address _to, uint256 _tokenId)
Grants approval for address _to
to take possession of the NFT with ID _tokenId
. This method MUST throw
if msg.sender != ownerOf(_tokenId)
, or if _tokenID
does not represent an NFT currently tracked by this contract, or if msg.sender == _to
.
Only one address can "have approval" at any given time; calling approveTransfer
with a new address revokes approval for the previous address. Calling this method with 0 as the _to
argument clears approval for any address.
Successful completion of this method MUST emit an Approval
event (defined below) unless the caller is attempting to clear approval when there is no pending approval. In particular, an Approval event MUST be fired if the _to
address is zero and there is some outstanding approval. Additionally, an Approval event MUST be fired if _to
is already the currently approved address and this call otherwise has no effect. (i.e. An approve()
call that "reaffirms" an existing approval MUST fire an event.)
Action | Prior State | _to address | New State | Event |
---|---|---|---|---|
Clear unset approval | Clear | 0 | Clear | None |
Set new approval | Clear | X | Set to X | Approval(owner, X, tokenID) |
Change approval | Set to X | Y | Set to Y | Approval(owner, Y, tokenID) |
Reaffirm approval | Set to X | X | Set to X | Approval(owner, X, tokenID) |
Clear approval | Set to X | 0 | Clear | Approval(owner, 0, tokenID) |
Note: ANY change of ownership of an NFT – whether directly through the transfer
and transferFrom
methods defined in this interface, or through any other mechanism defined in the conforming contract – MUST clear any and all approvals for the transferred NFT. The implicit clearing of approval via ownership transfer MUST also fire the event Approval(0, _tokenId)
if there was an outstanding approval. (i.e. All actions that transfer ownership must emit the same Approval event, if any, as would emitted by calling approve(0, _tokenID)
.)
takeOwnership
function takeOwnership(uint256 _tokenId)
Assigns the ownership of the NFT with ID _tokenId
to msg.sender
if and only if msg.sender
currently has approval (via a previous call to approveTransfer
). A successful transfer MUST fire the Transfer
event (defined below).
This method MUST transfer ownership to msg.sender
or throw
, no other outcomes can be possible. Reasons for failure include (but are not limited to):
msg.sender
does not have approval for_tokenId
_tokenID
does not represent an NFT currently tracked by this contractmsg.sender
already has ownership of_tokenId
Important: Please refer to the Note in the approveTransfer
method description; a successful transfer MUST clear pending approval.
transfer
function transfer(address _to, uint256 _tokenId)
Assigns the ownership of the NFT with ID _tokenId
to _to
if and only if msg.sender == ownerOf(_tokenId)
. A successful transfer MUST fire the Transfer
event (defined below).
This method MUST transfer ownership to _to
or throw
, no other outcomes can be possible. Reasons for failure include (but are not limited to):
msg.sender
is not the owner of_tokenId
_tokenID
does not represent an NFT currently tracked by this contract_to
is 0 (Conforming contracts MAY have other methods to destroy or burn NFTs, which are conceptually "transfers to 0" and will emitTransfer
events reflecting this. However,transfer(0, tokenID)
MUST be treated as an error.)
A conforming contract MUST allow the current owner to "transfer" a token to themselves, as a way of affirming ownership in the event stream. (i.e. it is valid for _to == ownerOf(_tokenID)
.) This "no-op transfer" MUST be considered a successful transfer, and therefore MUST fire a Transfer
event (with the same address for _from
and _to
).
Important: Please refer to the Note in the approveTransfer
method description; a successful transfer MUST clear pending approval. This includes no-op transfers to the current owner!
tokenOfOwnerByIndex
function tokenOfOwnerByIndex(address _owner, uint256 _index) constant returns (uint tokenId)
OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.
Returns the nth NFT assigned to the address _owner
, with n specified by the _index
argument. This method MUST throw
if _index >= balanceOf(_owner)
.
Recommended usage is as follows:
uint256 ownerBalance = nonFungibleContract.balanceOf(owner);
uint256[] memory ownerTokens = new uint256[](ownerBalance);
for (uint256 i = 0; i < ownerBalance; i++) {
ownerTokens[i] = nonFungibleContract.tokenOfOwnerByIndex(owner, i);
}
Implementations MUST NOT assume that NFTs are accessed in any particular order by their callers (In particular, don't assume this method is called in a monotonically ascending loop.), and MUST ensure that calls to tokenOfOwnerByIndex
are fully idempotent unless and until some non-constant
function is called on this contract.
Callers of tokenOfOwnerByIndex
MUST never assume that the order of NFTs is maintained outside of a single operation, or through the invocation (direct or indirect) of any non-constant
contract method.
NOTE: Current limitations in Solidity mean that there is no efficient way to return a complete list of an address's NFTs with a single function call. Callers should not assume this method is implemented efficiently (from a gas standpoint) and should strenuously avoid calling this method "on-chain" (i.e. from any non-constant
contract function, or from any constant
contract function that is likely to be called on-chain).
NFT Metadata
tokenMetadata
function tokenMetadata(uint256 _tokenId) constant returns (string infoUrl)
OPTIONAL - It is recommend that this method is implemented for enhanced usability with wallets and exchanges, but interfaces and other contracts MUST NOT depend on the existence of this method.
Returns a multiaddress string referencing an external resource bundle that contains (optionally localized) metadata about the NFT associated with _tokenId
. The string MUST be an IPFS or HTTP(S) base path (without a trailing slash) to which specific subpaths are obtained through concatenation. (IPFS is the preferred format due to better scalability, persistence, and immutability.)
Standard sub-paths:
- name (required) - The
name
sub-path MUST contain the UTF-8 encoded name of the specific NFT (i.e. distinct from the name of the collection, as returned by the contract'sname
method). A name SHOULD be 50 characters or less, and unique amongst all NFTs tracked by this contract. A name MAY contain white space characters, but MUST NOT include new-line or carriage-return characters. A name MAY include a numeric component to differentiate from similar NFTs in the same contract. For example: "Happy Token Dynamic gas pricing for opcodes (miner decided opcode pricing) #157". - image (optional) - If the
image
sub-path exists, it MUST contain a PNG, JPEG, or SVG image with at least 300 pixels of detail in each dimension. The image aspect ratio SHOULD be between 16:9 (landscape mode) and 2:3 (portrait mode). The image SHOULD be structured with a "safe zone" such that cropping the image to a maximal, central square doesn't remove any critical information. (The easiest way to meet this requirement is simply to use a 1:1 image aspect ratio.) - description (optional) - If the
description
sub-path exists, it MUST contain a UTF-8 encoded textual description of the asset. This description MAY contain multiple lines and SHOULD use a single new-line character to delimit explicit line-breaks, and two new-line characters to delimit paragraphs. The description MAY include CommonMark-compatible Markdown annotations for styling. The description SHOULD be 1500 characters or less. - other metadata (optional) - A contract MAY choose to include any number of additional subpaths, where they are deemed useful. There may be future formal and informal standards for additional metadata fields independent of this standard.
Each metadata subpath (including subpaths not defined in this standard) MUST contain a sub-path default
leading to a file containing the default (i.e. unlocalized) version of the data for that metadata element. For example, an NFT with the metadata path /ipfs/QmZU8bKEG8fhcQwKoLHfjtJoKBzvUT5LFR3f8dEz86WdVe
MUST contain the NFT's name as a UTF-8 encoded string available at the full path /ipfs/QmZU8bKEG8fhcQwKoLHfjtJoKBzvUT5LFR3f8dEz86WdVe/name/default
. Additionally, each metadata subpath MAY have one or more localizations at a subpath of an ISO 639-1 language code (the same language codes used for HTML). For example, /ipfs/QmZU8bKEG8fhcQwKoLHfjtJoKBzvUT5LFR3f8dEz86WdVe/name/en
would have the name in English, and /ipfs/QmZU8bKEG8fhcQwKoLHfjtJoKBzvUT5LFR3f8dEz86WdVe/name/fr
would have the name in French (note that even localized values need to have a default
entry). Consumers of NFT metadata SHOULD look for a localized value before falling back to the default
value. Consumers MUST NOT assume that all metadata subpaths for a particular NFT are localized similarly. For example, it will be common for the name
and image
objects to not be localized even when the description
is.
You can explore the metadata package referenced in this example here.
Events
Transfer
This event MUST trigger when NFT ownership is transferred via any mechanism.
Additionally, the creation of new NFTs MUST trigger a Transfer event for each newly created NFTs, with a _from
address of 0 and a _to
address matching the owner of the new NFT (possibly the smart contract itself). The deletion (or burn) of any NFT MUST trigger a Transfer event with a _to
address of 0 and a _from
address of the owner of the NFT (now former owner!).
NOTE: A Transfer event with _from == _to
is valid. See the transfer()
documentation for details.
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId)
Approval
This event MUST trigger on any successful call to approve(address _spender, uint256 _value)
(unless the caller is attempting to clear approval when there is no pending approval).
See the documentation for the approve()
method above for further detail.
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId)
Rationale
Utility
There are many proposed uses of Ethereum smart contracts that depend on tracking individual, non-fungible tokens (NFTs). Examples of existing or planned NFTs are LAND in Decentraland, the eponymous punks in CryptoPunks, and in-game items using systems like Dmarket or EnjinCoin. Future uses include tracking real-world non-fungible assets, like real-estate (as envisioned by companies like Ubitquity or Propy). It is critical in each of these cases that these items are not "lumped together" as numbers in a ledger, but instead, each token must have its ownership individually and atomically tracked. Regardless of the nature of these items, the ecosystem will be stronger if we have a standardized interface that allows for cross-functional non-fungible token management and sales platforms.
NTF IDs
The basis of this standard is that every NFT is identified by a unique, 256-bit unsigned integer within its tracking contract. This ID number MUST NOT change for the life of the contract. The pair (contract address, asset ID)
will then be a globally unique and fully-qualified identifier for a specific NFT within the Ethereum ecosystem. While some contracts may find it convenient to start with ID 0 and simply increment by one for each new NFT, callers MUST NOT assume that ID numbers have any specific pattern to them, and should treat the ID as a "black box".
Backwards Compatibility
This standard follows the semantics of ERC-20 as closely as possible, but can't be entirely compatible with it due to the fundamental differences between fungible and non-fungible tokens.
Example non-fungible implementations as of September 2017:
- CryptoPunks - Partially ERC-20 compatible, but not easily generalizable because it includes auction functionality directly in the contract and uses function names that explicitly refer to the NFTs as "punks".
- Auctionhouse Asset Interface - @dob needed a generic interface for his Auctionhouse dapp (currently ice-boxed). His "Asset" contract is very simple, but is missing ERC-20 compatibility, approve() functionality, and metadata. This effort is referenced in the discussion for EIP-173.
(It should be noted that "limited edition, collectable tokens" like Curio Cards and Rare Pepe are not non-fungible tokens. They're actually a collection of individual fungible tokens, each of which is tracked by its own smart contract with its own total supply (which may be 1
in extreme cases).)
Implementation
Reference implementation forthcoming...
Copyright
Copyright and related rights waived via CC0.