Skip to content

ERC: Non-fungible Token Standard #721

Closed
@dete

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() 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.

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

    Issue actions