Skip to content

ERC: Non-fungible Token Standard #721

Closed
@dete

Description

@dete

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() optional
    • symbol() optional
    • totalSupply() - Number of NFTs tracked by this contract
    • balanceOf(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 any transfer() operation
    • transferFrom(address _from, address _to, unit _tokenId) - the sender must have been previously authorized by approve(). (Note: Technically, the _from address here can be inferred by calling ownerOf(_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 the approve authorization inside a successful transfer call.)
  • NFT metadata (optional):
    • tokenMetadata(uint _tokenId) returns (string infoUrl) - recommended format is IPFS or HTTP multiaddress with name, image, and description sub-paths. IPFS is the preferred mechanism (immutable and more durable). Example: If tokenMetadata() returns /ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG, the object description would be accessible via ipfs 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 contract
  • msg.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 emit Transfer 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's name 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.

Activity

dob

dob commented on Sep 22, 2017

@dob

You could consider using an ipfs multiaddr as the return value for the token metadata. This would allow for a self describing IPFS hash, http url, or swarm addr in the future.

dete

dete commented on Sep 22, 2017

@dete
Author

Great point, @dob. Thanks for the suggestion! I've updated the draft above.

(For reference, my original posting had suggested using the format ipfs:QmYwAP... for IPFS links.)

kylerchin

kylerchin commented on Sep 23, 2017

@kylerchin

wow! awesome integration! This is better than sending the raw data over the wire. :)

Arachnid

Arachnid commented on Sep 23, 2017

@Arachnid
Contributor

Nice proposal! For the metadata to be useful, though, I think you need to mandate its format, rather than just recommend it.

GNSPS

GNSPS commented on Sep 23, 2017

@GNSPS

A much needed EIP! 👏

ethernian

ethernian commented on Sep 23, 2017

@ethernian

I would propose to use term "asset" instead of "nun-fungible token" to separate both terms.
It is an usual mistake to take some (fungible) token as representation of some (non-fungible) physical asset. So we can make it clear from very beginning.

dete

dete commented on Sep 23, 2017

@dete
Author

@Arachnid: The whole metadata method is optional, but if someone is going to implement it, they should probably implement it in a way that everyone else is expecting. I anticipate using "SHOULD" language for the full specification.

@dip239: Ah naming things. Every programmer's favourite bikeshedding vortex. 😁 I bounced between three separate ideas for naming: "token" as seen above, "asset", following @dob's lead (and your suggestion!), and "NFT" which I used in the prose, but which is incredibly awkward in the API calls: nftMetadata(uint _nftId).

"Asset" does seem like a decent choice, and my first personal draft used it. I discarded it for a two key reasons:

  • The definition of "asset" in a financial context says nothing about fungibility. Cash is an asset (fungible), so are shares (fungible), so is real-estate (non-fungible). ¯\_(ツ)_/¯
  • On the other hand, the definition of "asset" in a financial context directly implies financial value, something that must be declared on one's balance sheet. I believe one of the motivations between using the term "token" for ERC-20 instead of "coin" was specifically to avoid the implication of financial value at a time (which I would argue we are still in) when the crypto community wanted to avoid giving ammunition to traditional regulators—who don't really understand all the implications of what we're doing here.

I did spend some time trying out other alternatives ("item", "object", "thing", etc.), but none seemed right. I am happy to hear other suggestions because I am not entirely content with "token". It just seems like the best of some bad options.

silasdavis

silasdavis commented on Sep 24, 2017

@silasdavis

@dete your justification for NFT sounds rational to me, but allowing myself to plunge into the vortex for a moment. Sometimes finding a word that is relatively unreserved in modern usage but means the same thing works. 'Tesserae' were ancient tokens or tiles used as theatre tickets, forms of religious authentication, etc (see: https://www2.warwick.ac.uk/fac/arts/classics/research/dept_projects/tcam/about/, https://www2.warwick.ac.uk/fac/arts/classics/research/dept_projects/tcam/blog/, https://en.wikipedia.org/wiki/Tessera_(commerce)). Seems like they did function as a kind of non-fungible potentially value-holding token, but were quite varied in their specific application, which might suit the present case. Then again maybe it is just trying a bit too hard...

Arachnid

Arachnid commented on Sep 25, 2017

@Arachnid
Contributor

@dete I think you SHOULD use MUST instead. Otherwise, callers have no way to know how to interpret the return value correctly.

tjayrush

tjayrush commented on Sep 25, 2017

@tjayrush

Possible (but unlikely) names: ticket, badge, wafer, tile, marker

dete

dete commented on Sep 28, 2017

@dete
Author

@silasdavis: If this were a whole project, and not a single interface that is part of something larger, I'd jump on Tessera in a heartbeat. In that situation, a bit of an unusual name – with some history and context behind it – is pretty compelling. For something like an interface, tho, it's probably much better to stick with a term that people are already familiar with; even if it's imperfect.

@tjayrush: Thanks for the suggestions! I don't think most of them work well, but "marker" might. I'm definitely going to stew on that one a bit more. If it weren't overloaded in such common usage as a writing implement, it would probably be just about perfect.

dete

dete commented on Sep 28, 2017

@dete
Author

@Arachnid: Happy to follow the community lead here, but if you look at ERC-20, they use "SHOULD" language for something as foundational as emitting Transfer events. The metadata structure felt no more critical to me than Transfer...

MicahZoltu

MicahZoltu commented on Sep 29, 2017

@MicahZoltu
Contributor

Do not use ERC20 as an example of a good standard. A ton of people went and implemented the draft and then we had to finalize a "standard" that basically just listed what other people were doing. I don't believe anyone sees ERC20 as a good standard and everyone I have spoken to would like to see new better standards (hence why ERC223 exists).

Specifically, ERC20 used SHOULD because the standard came after a bunch of implementations and not all implementations did the same thing, so the standard couldn't say MUST without causing a bunch of ERC20-like tokens to not be ERC20.

dete

dete commented on Sep 29, 2017

@dete
Author

Great context, @MicahZoltu. Thank you!

Arachnid

Arachnid commented on Oct 2, 2017

@Arachnid
Contributor

@dete I'd also argue that the format of something is more foundational than whether you emit it or not. If you don't emit a transfer event, others can't track token transfers for your token - but if you don't require the format of a field, then they can't parse it anywhere.

473 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @superphly@dob@Arachnid@dekz@eordano

        Issue actions

          ERC: Non-fungible Token Standard · Issue #721 · ethereum/EIPs