New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC: Non-fungible Token Standard #721

Open
dete opened this Issue Sep 22, 2017 · 363 comments

Comments

Projects
None yet
@dete

dete commented Sep 22, 2017

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 #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.

@dob

This comment has been minimized.

Show comment
Hide comment
@dob

dob Sep 22, 2017

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.

dob commented Sep 22, 2017

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

This comment has been minimized.

Show comment
Hide comment
@dete

dete Sep 22, 2017

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

dete commented Sep 22, 2017

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

This comment has been minimized.

Show comment
Hide comment
@kylerchin

kylerchin Sep 23, 2017

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

kylerchin commented Sep 23, 2017

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

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Sep 23, 2017

Collaborator

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

Collaborator

Arachnid commented Sep 23, 2017

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

@GNSPS

This comment has been minimized.

Show comment
Hide comment
@GNSPS

GNSPS Sep 23, 2017

A much needed EIP! 👏

GNSPS commented Sep 23, 2017

A much needed EIP! 👏

@dip239

This comment has been minimized.

Show comment
Hide comment
@dip239

dip239 Sep 23, 2017

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.

dip239 commented Sep 23, 2017

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

This comment has been minimized.

Show comment
Hide comment
@dete

dete Sep 23, 2017

@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.

dete commented Sep 23, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@silasdavis

silasdavis Sep 24, 2017

@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...

silasdavis commented Sep 24, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Sep 25, 2017

Collaborator

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

Collaborator

Arachnid commented Sep 25, 2017

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

@tjayrush

This comment has been minimized.

Show comment
Hide comment
@tjayrush

tjayrush Sep 25, 2017

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

tjayrush commented Sep 25, 2017

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

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Sep 28, 2017

@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 commented Sep 28, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@dete

dete Sep 28, 2017

@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...

dete commented Sep 28, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@MicahZoltu

MicahZoltu Sep 29, 2017

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.

Contributor

MicahZoltu commented Sep 29, 2017

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

This comment has been minimized.

Show comment
Hide comment
@dete

dete Sep 29, 2017

Great context, @MicahZoltu. Thank you!

dete commented Sep 29, 2017

Great context, @MicahZoltu. Thank you!

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Oct 2, 2017

Collaborator

@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.

Collaborator

Arachnid commented Oct 2, 2017

@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.

@eordano

This comment has been minimized.

Show comment
Hide comment
@eordano

eordano Oct 6, 2017

Hey @dete! I went ahead and coded a first draft of this. Names are slightly different.
https://github.com/decentraland/land/blob/master/contracts/BasicNFT.sol

eordano commented Oct 6, 2017

Hey @dete! I went ahead and coded a first draft of this. Names are slightly different.
https://github.com/decentraland/land/blob/master/contracts/BasicNFT.sol

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Oct 7, 2017

So, I was talking to @flockonus (another Solidity engineer here) and I was worrying about the "lost token" problem that ERC-223 tries to solve. (Essentially, ERC-20 has no mechanism to keep users from sending their tokens to contracts that don't know how to handle them, resulting in entirely unrecoverable coins. @Dexaran did some analysis estimating that something like $400k has been lost in this way!)

The solution proposed by ERC-223 is to only allow transfers to contract addresses that implement the tokenFallback method. This has the nice side effect that the receiving contract gets a chance to do some work when it receives the coins.

I talked to @flockonus because I was hesitant to add more complexity to NFTs, and he kind of floored me with a suggestion that seemed crazy at first, but is really growing on me: Just get rid of the transfer method.

So hear me out: What if the only way to transfer an NFT was for the owner to approve a recipient, and have the recipient (whether contract or non-contract) call takeOwnership (which works like transferFrom, but with msg.sender hard-coded as the _to parameter).

This solves three problems, two of which are shared with fungible tokens (and addressed by ERC-223), one that is specific to NFTs:

  1. Sending assets to contracts that don't know how to handle them. That becomes impossible if there is no direct transfer call; the contract can't be given a token, it has to call takeOwnership.
  2. Contracts should know when they get new tokens. Well, since the contract has to explicitly takeOwnership, the flow for sending a token to a contract would be approve(contract) followed by contract.someMethod(). someMethod that would takeOwnership, plus the kind of things you'd put into tokenFallback (if appropriate).
  3. Some low-value NFTs could end up being something akin to "spam". The easiest motivating example would be "trash" items in a video game; no game I know of allows griefers to shove items in your backpack without your consent. (This could be especially nasty in a smart-contract context where certain operations may be O(n) on the number of NFTs you hold.)

It introduces a new problem, of course: The simplest conceivable operation (transferring ownership from user to user) now requires two transactions instead of one. But how much of a problem is this, really? As time goes on, we'll see more and more transactions mediated by exchanges, smart contracts, UIs, and automated agents.

One possible option would be to include transfer but make it optional (meaning that contracts would know not to use it), and strongly recommend that it throws if targeting a contract address. (Although that wouldn't solve the spammy NFT problem.)

I'd love to hear other folks thoughts!

dete commented Oct 7, 2017

So, I was talking to @flockonus (another Solidity engineer here) and I was worrying about the "lost token" problem that ERC-223 tries to solve. (Essentially, ERC-20 has no mechanism to keep users from sending their tokens to contracts that don't know how to handle them, resulting in entirely unrecoverable coins. @Dexaran did some analysis estimating that something like $400k has been lost in this way!)

The solution proposed by ERC-223 is to only allow transfers to contract addresses that implement the tokenFallback method. This has the nice side effect that the receiving contract gets a chance to do some work when it receives the coins.

I talked to @flockonus because I was hesitant to add more complexity to NFTs, and he kind of floored me with a suggestion that seemed crazy at first, but is really growing on me: Just get rid of the transfer method.

So hear me out: What if the only way to transfer an NFT was for the owner to approve a recipient, and have the recipient (whether contract or non-contract) call takeOwnership (which works like transferFrom, but with msg.sender hard-coded as the _to parameter).

This solves three problems, two of which are shared with fungible tokens (and addressed by ERC-223), one that is specific to NFTs:

  1. Sending assets to contracts that don't know how to handle them. That becomes impossible if there is no direct transfer call; the contract can't be given a token, it has to call takeOwnership.
  2. Contracts should know when they get new tokens. Well, since the contract has to explicitly takeOwnership, the flow for sending a token to a contract would be approve(contract) followed by contract.someMethod(). someMethod that would takeOwnership, plus the kind of things you'd put into tokenFallback (if appropriate).
  3. Some low-value NFTs could end up being something akin to "spam". The easiest motivating example would be "trash" items in a video game; no game I know of allows griefers to shove items in your backpack without your consent. (This could be especially nasty in a smart-contract context where certain operations may be O(n) on the number of NFTs you hold.)

It introduces a new problem, of course: The simplest conceivable operation (transferring ownership from user to user) now requires two transactions instead of one. But how much of a problem is this, really? As time goes on, we'll see more and more transactions mediated by exchanges, smart contracts, UIs, and automated agents.

One possible option would be to include transfer but make it optional (meaning that contracts would know not to use it), and strongly recommend that it throws if targeting a contract address. (Although that wouldn't solve the spammy NFT problem.)

I'd love to hear other folks thoughts!

@Dexaran

This comment has been minimized.

Show comment
Hide comment
@Dexaran

Dexaran Oct 7, 2017

@dete It's an interesting idea, but I think that the problems of this approach outweigh its advantages because:

  1. Ethereum suffers bandwidth issues. I think that requiring that each token move is performed with two transactions is an irrational use of the blockchain.

  2. A couple of transactions will require additional gas so it will be more expensive.

  3. Approving then withdrawing is not a common pattern of sending funds in the cryptocurrency world. You can send ETH, ETC, Bitcoins, PIVX and even Doge without any confirmation from the side of the receiver. This will not be an intuitive-clear for users.

  4. The ideology of uniformity. ETH and tokens are currencies. I think that it is better to make them behave similar.

  5. The problems that you try to solve with this proposal are already solved with ERC223. I don't see any value in solving already-solved problems.

Dexaran commented Oct 7, 2017

@dete It's an interesting idea, but I think that the problems of this approach outweigh its advantages because:

  1. Ethereum suffers bandwidth issues. I think that requiring that each token move is performed with two transactions is an irrational use of the blockchain.

  2. A couple of transactions will require additional gas so it will be more expensive.

  3. Approving then withdrawing is not a common pattern of sending funds in the cryptocurrency world. You can send ETH, ETC, Bitcoins, PIVX and even Doge without any confirmation from the side of the receiver. This will not be an intuitive-clear for users.

  4. The ideology of uniformity. ETH and tokens are currencies. I think that it is better to make them behave similar.

  5. The problems that you try to solve with this proposal are already solved with ERC223. I don't see any value in solving already-solved problems.

@ryanschneider

This comment has been minimized.

Show comment
Hide comment
@ryanschneider

ryanschneider Oct 13, 2017

Some low-value NFTs could end up being something akin to "spam". The easiest motivating example would be "trash" items in a video game; no game I know of allows griefers to shove items in your backpack without your consent. (This could be especially nasty in a smart-contract context where certain operations may be O(n) on the number of NFTs you hold.)

I thought about this some, and is it really that big of an issue? With the current ERC, each spam item would require its own transaction, so it would be a rather expensive attack, right?

That said, what if there were two new optional methods:

approveMultiple(address _to, uint[] _tokenIds)
transferMultipleFrom(address _from, address _to, uint[] _tokenIds)

These would allow "high volume" NFT contracts (like your MMO item example) to do "approved" bulk transfers, while the lower volume contracts would be naturally protected from spam via the requirement to send a single tokenId per transaction using the standard transfer call.

These methods would also allow one to merge/move wallets w/ 2 transactions instead of the N transactions currently required to move one item at a time.

ryanschneider commented Oct 13, 2017

Some low-value NFTs could end up being something akin to "spam". The easiest motivating example would be "trash" items in a video game; no game I know of allows griefers to shove items in your backpack without your consent. (This could be especially nasty in a smart-contract context where certain operations may be O(n) on the number of NFTs you hold.)

I thought about this some, and is it really that big of an issue? With the current ERC, each spam item would require its own transaction, so it would be a rather expensive attack, right?

That said, what if there were two new optional methods:

approveMultiple(address _to, uint[] _tokenIds)
transferMultipleFrom(address _from, address _to, uint[] _tokenIds)

These would allow "high volume" NFT contracts (like your MMO item example) to do "approved" bulk transfers, while the lower volume contracts would be naturally protected from spam via the requirement to send a single tokenId per transaction using the standard transfer call.

These methods would also allow one to merge/move wallets w/ 2 transactions instead of the N transactions currently required to move one item at a time.

@flockonus

This comment has been minimized.

Show comment
Hide comment
@flockonus

flockonus Oct 18, 2017

About the point of removing the transfer method, we've been thinking heavily about this for CryptoKitties (disclosure i'm in Dete's team), and altho we have unconditional transfer method implemented for the alpha, it's easy to see why unconditional transfer might not work in multiple cases.

To start off, we have to understand that while a similar API to ERC20 is desirable, the case it aims to solve is different. In ERC20 the abstraction is the more you have the better because the tokens have positive monetary value, and go into an indistinguishable sum.

That's not the case with non-fungible, either with a smart contract that tracks property ownership such as the one Dubai is implementing, or a game with relatively lower value assets, they might not always carry positive value or be desirable.

So even with due consideration about it being an expensive attack, when designing a mechanism we should take into account that some users will have significant more purchase power than others, and still they shouldn't be able to harm others with less.

My point is, if transfer is implemented it should be expected to throw depending on the smart contract implementation of what user decides to accept what. The business rules of accepting a token would definitely vary for each S.C. implementation.

flockonus commented Oct 18, 2017

About the point of removing the transfer method, we've been thinking heavily about this for CryptoKitties (disclosure i'm in Dete's team), and altho we have unconditional transfer method implemented for the alpha, it's easy to see why unconditional transfer might not work in multiple cases.

To start off, we have to understand that while a similar API to ERC20 is desirable, the case it aims to solve is different. In ERC20 the abstraction is the more you have the better because the tokens have positive monetary value, and go into an indistinguishable sum.

That's not the case with non-fungible, either with a smart contract that tracks property ownership such as the one Dubai is implementing, or a game with relatively lower value assets, they might not always carry positive value or be desirable.

So even with due consideration about it being an expensive attack, when designing a mechanism we should take into account that some users will have significant more purchase power than others, and still they shouldn't be able to harm others with less.

My point is, if transfer is implemented it should be expected to throw depending on the smart contract implementation of what user decides to accept what. The business rules of accepting a token would definitely vary for each S.C. implementation.

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Nov 8, 2017

Upon further reflection and discussion with @flockonus, we are proposing to keep a straightforward (and therefore "unsafe") transfer() method. (i.e. It would work like ERC-20, and not EIP-223.)

Our reasoning is as follows:

  • Simpler is always better for standards. The fewer requirements, the harder it is to screw it up. (And NFTs are already complicated enough!)
  • It is not the job of a smart contract to protect against every possible user error (it must protect against invalid actions, not unintended actions). In particular, the case we are trying to avoid (sending NFTs to contracts that don't know how to handle them) is better served by checks and warnings in the wallet software. A wallet (or other smart contract interface) will always be able to have more robust and dynamic checks than a smart contract, and is also able to engage in some "back-and-forth" dialog with the user. Smart contracts don't have the equivalent of a "This seems unsafe, are you sure?" dialog box!

Similarly, as @flockonus mentioned about the potential "spam" problem: This is not a problem that the community standard needs to solve. If an implementor of an ERC-721 contract feels like spam is likely to be a problem with their NFT, they are welcome to include additional functionality to make it easy for users to mark their accounts as not accepting unsolicited transfers.

dete commented Nov 8, 2017

Upon further reflection and discussion with @flockonus, we are proposing to keep a straightforward (and therefore "unsafe") transfer() method. (i.e. It would work like ERC-20, and not EIP-223.)

Our reasoning is as follows:

  • Simpler is always better for standards. The fewer requirements, the harder it is to screw it up. (And NFTs are already complicated enough!)
  • It is not the job of a smart contract to protect against every possible user error (it must protect against invalid actions, not unintended actions). In particular, the case we are trying to avoid (sending NFTs to contracts that don't know how to handle them) is better served by checks and warnings in the wallet software. A wallet (or other smart contract interface) will always be able to have more robust and dynamic checks than a smart contract, and is also able to engage in some "back-and-forth" dialog with the user. Smart contracts don't have the equivalent of a "This seems unsafe, are you sure?" dialog box!

Similarly, as @flockonus mentioned about the potential "spam" problem: This is not a problem that the community standard needs to solve. If an implementor of an ERC-721 contract feels like spam is likely to be a problem with their NFT, they are welcome to include additional functionality to make it easy for users to mark their accounts as not accepting unsolicited transfers.

@ryanschneider

This comment has been minimized.

Show comment
Hide comment
@ryanschneider

ryanschneider Nov 8, 2017

Is transfer() still accepting a single NFT tokenId? Any thoughts on my suggestion of defining some optional methods to support bulk transfers?

Say I decide to sell a large portion of my NFT collection to someone else, and it contains hundreds of NFTs (or more). Shouldn't there be a standard way to transfer N items atomically?

I can see the point of trying to keep the ERC simple, but am concerned that w/o a solution for bulk transfers the scope of what NFTs the ERC can be viable for is limited.

ryanschneider commented Nov 8, 2017

Is transfer() still accepting a single NFT tokenId? Any thoughts on my suggestion of defining some optional methods to support bulk transfers?

Say I decide to sell a large portion of my NFT collection to someone else, and it contains hundreds of NFTs (or more). Shouldn't there be a standard way to transfer N items atomically?

I can see the point of trying to keep the ERC simple, but am concerned that w/o a solution for bulk transfers the scope of what NFTs the ERC can be viable for is limited.

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Nov 9, 2017

Here is the new draft, which is basically the first "complete" draft. Any and all comments are welcome!


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 #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.

dete commented Nov 9, 2017

Here is the new draft, which is basically the first "complete" draft. Any and all comments are welcome!


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 #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.

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Nov 9, 2017

Collaborator

@dete You should update the initial issue with this draft, so people coming new to it don't need to scroll through all the comments.

Collaborator

Arachnid commented Nov 9, 2017

@dete You should update the initial issue with this draft, so people coming new to it don't need to scroll through all the comments.

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Nov 10, 2017

Thanks @Arachnid: I put a link in the first comment for now, I don't want to lose the older version for anyone who wants to follow the conversation.

dete commented Nov 10, 2017

Thanks @Arachnid: I put a link in the first comment for now, I don't want to lose the older version for anyone who wants to follow the conversation.

@dete

This comment has been minimized.

Show comment
Hide comment
@dete

dete Nov 10, 2017

Does anyone have any thoughts on whether the _tokenID in the events should be indexed? I would think yes (it's easy to imagine that people or processes would want to watch for events relating to specific NFTs), but this comment in the Solidity docs on Events gave me pause:

NOTE: Indexed arguments will not be stored themselves. You can only search for the values, but it is impossible to retrieve the values themselves.

That seems... problematic...

dete commented Nov 10, 2017

Does anyone have any thoughts on whether the _tokenID in the events should be indexed? I would think yes (it's easy to imagine that people or processes would want to watch for events relating to specific NFTs), but this comment in the Solidity docs on Events gave me pause:

NOTE: Indexed arguments will not be stored themselves. You can only search for the values, but it is impossible to retrieve the values themselves.

That seems... problematic...

@Arachnid

This comment has been minimized.

Show comment
Hide comment
@Arachnid

Arachnid Nov 10, 2017

Collaborator

I don't believe that's quite accurate; you should be able to retrieve the contents of an indexed event (for a fixed length type) or its hash (for string/bytes/arrays).

Collaborator

Arachnid commented Nov 10, 2017

I don't believe that's quite accurate; you should be able to retrieve the contents of an indexed event (for a fixed length type) or its hash (for string/bytes/arrays).

@NoahMarconi

This comment has been minimized.

Show comment
Hide comment
@NoahMarconi

NoahMarconi Nov 10, 2017

Any thoughts on transferring multiple tokens in a single transfer?

We ran into this needs recently and the gas costs are making many options unfeasible.

NoahMarconi commented Nov 10, 2017

Any thoughts on transferring multiple tokens in a single transfer?

We ran into this needs recently and the gas costs are making many options unfeasible.

@jpitts

This comment has been minimized.

Show comment
Hide comment
@jpitts

jpitts Nov 10, 2017

Member

For metadata, I would strongly recommend requiring a standard URI.

Perhaps consider a way to allow for linkage to the same metadata hosted on different storage networks (i.e. additional parameter specifying 'ipfs", 'swarm', or 'https'). This way the metadata has a better chance of persisting.

Additionally, instead of an external resource bundle, I would recommend using JSON-LD. This format allows for more complex data, has wide use and tooling (being JSON), and provides context to that data

https://json-ld.org/.

Member

jpitts commented Nov 10, 2017

For metadata, I would strongly recommend requiring a standard URI.

Perhaps consider a way to allow for linkage to the same metadata hosted on different storage networks (i.e. additional parameter specifying 'ipfs", 'swarm', or 'https'). This way the metadata has a better chance of persisting.

Additionally, instead of an external resource bundle, I would recommend using JSON-LD. This format allows for more complex data, has wide use and tooling (being JSON), and provides context to that data

https://json-ld.org/.

@nadavhollander

This comment has been minimized.

Show comment
Hide comment
@nadavhollander

nadavhollander Nov 10, 2017

First -- greed with @jpitts on creating redundant storage of metadata on different storage networks and, more importantly IMO, being more unopinionated with respect to the data storage network. I imagine a URI format along the lines of "swarm://XYZ" / "ipfs://XYZ" with the URI prefix specifying the specific storage network queried.

Secondly, I'm working on tokenized debt issuance protocol (Dharma) and a big feature that would be extremely helpful to us is having built-in optional fungibility within each non-fungible asset. This may sound at odds with the purpose of NFTs, but I'll use Dharma as a tangible example:

Alice wants to issue a bond token using Dharma, and she wants to be able to sell individual shares in that bond to different creditors. She could ostensibly have an NFT representing the debt asset as a whole, wrap that NFT into another fungible ERC20 token contract, and sell tokens from that contract to creditors, but that would require her to incur the gas associated with deploying an entirely new token contract for a set of very generic token transfer functionality. It would be much simpler and cheaper to be able to ask the contract to mint an NFT with its own fractional supply of X tokens. In a sense, this would mean that individual debts would be non-fungible with one another, but within each debt there would be a fractional supply of tokens that are fungible with one another.

As an example, the function interface for transfer would look like this:

function transfer(address _to, uint256 _tokenId, uint246 _amount)

The standard could easily be made compatible with NFTs that have no fractional supply -- their issuance would simply have a fractional supply of 1. This is an advantageous arrangement for classes of digital assets that have (1) highly generic functionality and (2) require some sort of fractional fungibility and (2) don't merit the deployment of a smart contract for each issuance event.

With all the above being said, I would definitely not say this is a must have -- this arguably extends beyond the definition of non-fungibility to a certain degree. If, however, this is a very common need among projects that have similar dynamics to the tokens they're issuing, I think this would be the appropriate standard in which to include this functionality.

nadavhollander commented Nov 10, 2017

First -- greed with @jpitts on creating redundant storage of metadata on different storage networks and, more importantly IMO, being more unopinionated with respect to the data storage network. I imagine a URI format along the lines of "swarm://XYZ" / "ipfs://XYZ" with the URI prefix specifying the specific storage network queried.

Secondly, I'm working on tokenized debt issuance protocol (Dharma) and a big feature that would be extremely helpful to us is having built-in optional fungibility within each non-fungible asset. This may sound at odds with the purpose of NFTs, but I'll use Dharma as a tangible example:

Alice wants to issue a bond token using Dharma, and she wants to be able to sell individual shares in that bond to different creditors. She could ostensibly have an NFT representing the debt asset as a whole, wrap that NFT into another fungible ERC20 token contract, and sell tokens from that contract to creditors, but that would require her to incur the gas associated with deploying an entirely new token contract for a set of very generic token transfer functionality. It would be much simpler and cheaper to be able to ask the contract to mint an NFT with its own fractional supply of X tokens. In a sense, this would mean that individual debts would be non-fungible with one another, but within each debt there would be a fractional supply of tokens that are fungible with one another.

As an example, the function interface for transfer would look like this:

function transfer(address _to, uint256 _tokenId, uint246 _amount)

The standard could easily be made compatible with NFTs that have no fractional supply -- their issuance would simply have a fractional supply of 1. This is an advantageous arrangement for classes of digital assets that have (1) highly generic functionality and (2) require some sort of fractional fungibility and (2) don't merit the deployment of a smart contract for each issuance event.

With all the above being said, I would definitely not say this is a must have -- this arguably extends beyond the definition of non-fungibility to a certain degree. If, however, this is a very common need among projects that have similar dynamics to the tokens they're issuing, I think this would be the appropriate standard in which to include this functionality.

@mbeylin

This comment has been minimized.

Show comment
Hide comment
@mbeylin

mbeylin Jun 26, 2018

Contributor

Was there a reason why the transferFrom method doesn't return a bool similarly to how ERC20 tokens do? For contracts which desire to allow transactions in ERC20/721 tokens interchangeably, the ERC721 transferFrom function should return a bool to conform to the existing practise of wrapping a transferFrom call in a require to ensure it completes successfully.

Also, was there a reason why the default transfer function wasn't also included in the interface, to conform with the ERC20 interface?

Contributor

mbeylin commented Jun 26, 2018

Was there a reason why the transferFrom method doesn't return a bool similarly to how ERC20 tokens do? For contracts which desire to allow transactions in ERC20/721 tokens interchangeably, the ERC721 transferFrom function should return a bool to conform to the existing practise of wrapping a transferFrom call in a require to ensure it completes successfully.

Also, was there a reason why the default transfer function wasn't also included in the interface, to conform with the ERC20 interface?

@fulldecent

This comment has been minimized.

Show comment
Hide comment
@fulldecent

fulldecent Jun 26, 2018

Contributor

@mbeylin Yes, there is a good reason. That is very common question in this thread. Please open the standard at https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md and scroll down to the Rationale section to study more. The specific section you are interested in is "Transfer Mechanism".

Contributor

fulldecent commented Jun 26, 2018

@mbeylin Yes, there is a good reason. That is very common question in this thread. Please open the standard at https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md and scroll down to the Rationale section to study more. The specific section you are interested in is "Transfer Mechanism".

@mbeylin

This comment has been minimized.

Show comment
Hide comment
@mbeylin

mbeylin Jun 26, 2018

Contributor

@fulldecent I can see it noted that these alternatives were considered (and I notice they were originally recommended to be in the standard to maintain backwards compatibility), but the actual reasoning itself seems to be absent. Do you longer documentation on why transfer was omitted and the bool return (JUST FOR TRANSFERS) were removed?

Contributor

mbeylin commented Jun 26, 2018

@fulldecent I can see it noted that these alternatives were considered (and I notice they were originally recommended to be in the standard to maintain backwards compatibility), but the actual reasoning itself seems to be absent. Do you longer documentation on why transfer was omitted and the bool return (JUST FOR TRANSFERS) were removed?

@PhABC

This comment has been minimized.

Show comment
Hide comment
@PhABC

PhABC Jun 27, 2018

Contributor

I personally do not like the transfer() function anyway, since it's really a special case of transferFrom() where from is msg.sender. I personally prefer simpler interfaces in general and always complained about transfer() existing. Perhaps transfer() should've been implemented as transferFrom() in the first place, now it seems like we are stuck with the transferFrom() name for sometime :).

Contributor

PhABC commented Jun 27, 2018

I personally do not like the transfer() function anyway, since it's really a special case of transferFrom() where from is msg.sender. I personally prefer simpler interfaces in general and always complained about transfer() existing. Perhaps transfer() should've been implemented as transferFrom() in the first place, now it seems like we are stuck with the transferFrom() name for sometime :).

@mbeylin

This comment has been minimized.

Show comment
Hide comment
@mbeylin

mbeylin Jun 27, 2018

Contributor

While you're right that transfer() is not great, not including this function in the standard means that dapps must first approve() and then transferFrom() for ERC20 tokens as well if they want the contract to use the same code to handle both types of tokens (instead of being able to simply transfer them). Wouldn't it make sense to have the 721 interface mimic ERC20 in as many ways as possible?

Contributor

mbeylin commented Jun 27, 2018

While you're right that transfer() is not great, not including this function in the standard means that dapps must first approve() and then transferFrom() for ERC20 tokens as well if they want the contract to use the same code to handle both types of tokens (instead of being able to simply transfer them). Wouldn't it make sense to have the 721 interface mimic ERC20 in as many ways as possible?

@PhABC

This comment has been minimized.

Show comment
Hide comment
@PhABC

PhABC Jun 27, 2018

Contributor

I'm not sure I exactly understand. Most dapps I know don't use the transfer() function, but the transferFrom() function, unless the users deposit their tokens instead of calling approve(). If a contract wants to transfer via transferFrom() while the contract hold the tokens, they don't need to call approve() first either.

In general, I am in favor of reusing the same interface when possible, but when the use cases are different enough, I think it makes more sense to improve instead of getting stuck with legacy code. For instance, most ERC-721 token exchanges will be very different than ERC-20 token exchanges and that's fine, the markets aren't the same.

Contributor

PhABC commented Jun 27, 2018

I'm not sure I exactly understand. Most dapps I know don't use the transfer() function, but the transferFrom() function, unless the users deposit their tokens instead of calling approve(). If a contract wants to transfer via transferFrom() while the contract hold the tokens, they don't need to call approve() first either.

In general, I am in favor of reusing the same interface when possible, but when the use cases are different enough, I think it makes more sense to improve instead of getting stuck with legacy code. For instance, most ERC-721 token exchanges will be very different than ERC-20 token exchanges and that's fine, the markets aren't the same.

@fulldecent

This comment has been minimized.

Show comment
Hide comment
@fulldecent

fulldecent Jun 27, 2018

Contributor

@mbeylin Sorry, please see also #841 for further discussion, it is not all in this thread.

I expect most applications will use the simple and unsafe transferFrom or they will use the one-step safeTransferFrom. And the safety will be done by the app before calling those functions. However there are MANY transfer scenarios that are enabled that is why we have so many work flows. We really do have to wait to see how this stuff is going to be used in the wild to analyze further.

Contributor

fulldecent commented Jun 27, 2018

@mbeylin Sorry, please see also #841 for further discussion, it is not all in this thread.

I expect most applications will use the simple and unsafe transferFrom or they will use the one-step safeTransferFrom. And the safety will be done by the app before calling those functions. However there are MANY transfer scenarios that are enabled that is why we have so many work flows. We really do have to wait to see how this stuff is going to be used in the wild to analyze further.

@mbeylin

This comment has been minimized.

Show comment
Hide comment
@mbeylin

mbeylin Jun 27, 2018

Contributor

@PhABC

If a contract wants to transfer via transferFrom() while the contract hold the tokens, they don't need to call approve() first either.

This is true for EIP721 tokens yes but not true for ERC20 tokens. From every implementation I've seen, one must always call approve() first on the token before calling transferFrom() even if the from field is the msg.sender. For instance we use the default transfer() function in StandardBounties, and would desire to allow for our new contract to manage ERC20s and ERC721s seamlessly without intervention.

The key here is that new applications building on NFTs will happily use functions, but an app like bounties needs to let people pay in an amalgam of ERC20s (which are already minted) and new NFTs, and having to treat both cases separately introduces overhead that seems unnecessary.

Contributor

mbeylin commented Jun 27, 2018

@PhABC

If a contract wants to transfer via transferFrom() while the contract hold the tokens, they don't need to call approve() first either.

This is true for EIP721 tokens yes but not true for ERC20 tokens. From every implementation I've seen, one must always call approve() first on the token before calling transferFrom() even if the from field is the msg.sender. For instance we use the default transfer() function in StandardBounties, and would desire to allow for our new contract to manage ERC20s and ERC721s seamlessly without intervention.

The key here is that new applications building on NFTs will happily use functions, but an app like bounties needs to let people pay in an amalgam of ERC20s (which are already minted) and new NFTs, and having to treat both cases separately introduces overhead that seems unnecessary.

@PhABC

This comment has been minimized.

Show comment
Hide comment
@PhABC

PhABC Jun 27, 2018

Contributor

@mbeylin Yes, the overhead does increase, but it's very unlikely only two token standards will be successful and preparing for more seems like a more thorough approach. Although I find it strange people would pay bounties in ERC-721 (hard to efficiently value), there might be other standards where it make more sense. Ideally they all have compatible interfaces, but it's unlikely, especially in the next 5-10 years. We are already seeing a lot of token standards emerging and I don't think it will stop any time soon.

I personally think the new 0x approach is great, where their exchange contract will be able to add arbitrary token standard support using proxy contracts. Can have ERC-20 and 721 functions built in for efficiency while still allowing other arbitrary token standards via proxies.

In the end, cryptokitty showed the rest of the community that third parties will need to adapt to new, successful standards, otherwise they will be left out from a new and interesting market. I am not saying developers should not make any efforts to make new standards similar to existing ones, but I think the market and adoption will speak for itself anyway.

Contributor

PhABC commented Jun 27, 2018

@mbeylin Yes, the overhead does increase, but it's very unlikely only two token standards will be successful and preparing for more seems like a more thorough approach. Although I find it strange people would pay bounties in ERC-721 (hard to efficiently value), there might be other standards where it make more sense. Ideally they all have compatible interfaces, but it's unlikely, especially in the next 5-10 years. We are already seeing a lot of token standards emerging and I don't think it will stop any time soon.

I personally think the new 0x approach is great, where their exchange contract will be able to add arbitrary token standard support using proxy contracts. Can have ERC-20 and 721 functions built in for efficiency while still allowing other arbitrary token standards via proxies.

In the end, cryptokitty showed the rest of the community that third parties will need to adapt to new, successful standards, otherwise they will be left out from a new and interesting market. I am not saying developers should not make any efforts to make new standards similar to existing ones, but I think the market and adoption will speak for itself anyway.

@mudgen

This comment has been minimized.

Show comment
Hide comment
@mudgen

mudgen Jun 27, 2018

Contributor

The ERC721 standard says this:

The onERC721Received function specifically works around old deployed contracts which may inadvertently return 1 (true) in certain circumstances even if they don't implement a function (see Solidity DelegateCallReturnValue bug). By returning and checking for a magic value, we are able to distinguish actual affirmative responses versus these vacuous trues.

I would like to know in what circumstances will an old contracts return 1 when it doesn't implement a function. The standard only specifies the DelegateCallReturnValue bug which I don't think applies to the onERC721Received function. So in what cases does it actually make sense for onERC721Received to return a value?

Contributor

mudgen commented Jun 27, 2018

The ERC721 standard says this:

The onERC721Received function specifically works around old deployed contracts which may inadvertently return 1 (true) in certain circumstances even if they don't implement a function (see Solidity DelegateCallReturnValue bug). By returning and checking for a magic value, we are able to distinguish actual affirmative responses versus these vacuous trues.

I would like to know in what circumstances will an old contracts return 1 when it doesn't implement a function. The standard only specifies the DelegateCallReturnValue bug which I don't think applies to the onERC721Received function. So in what cases does it actually make sense for onERC721Received to return a value?

@fulldecent

This comment has been minimized.

Show comment
Hide comment
@fulldecent

fulldecent Jun 28, 2018

Contributor

@mudgen Please can we have that discussion on Stack Exchange. The 721 standard has pointed out the relevant rationale. But the discussion of DelegateCallReturnValue is applicable to anybody writing contracts.

Contributor

fulldecent commented Jun 28, 2018

@mudgen Please can we have that discussion on Stack Exchange. The 721 standard has pointed out the relevant rationale. But the discussion of DelegateCallReturnValue is applicable to anybody writing contracts.

@mudgen

This comment has been minimized.

Show comment
Hide comment
@mudgen

mudgen Jun 28, 2018

Contributor

@fulldecent Okay, I posted a question about this on Stack Exchange here: https://ethereum.stackexchange.com/questions/52258/what-are-good-specific-reasons-the-onerc721received-function-returns-a-magic-val

I feel the rationale for onERC721Received is incomplete and mentioning the DelegateCallReturnValue bug doesn't make sense because I don't know of any case using the delegatecall opcode to call onERC721Received makes sense.

Did someone actually know or verify the "circumstances" that contracts will inadvertently return true, besides the inapplicable DelegateCallReturnValue bug?

Edit:
I thought of two use cases where onERC721Received could be called with delegatecall:

  1. It could be used in a proxy contract.
  2. It could be used in a fallback function as in the example given here: https://ethereum.stackexchange.com/questions/37601/using-a-high-level-delegate-call-in-upgradable-contracts-since-byzantium
Contributor

mudgen commented Jun 28, 2018

@fulldecent Okay, I posted a question about this on Stack Exchange here: https://ethereum.stackexchange.com/questions/52258/what-are-good-specific-reasons-the-onerc721received-function-returns-a-magic-val

I feel the rationale for onERC721Received is incomplete and mentioning the DelegateCallReturnValue bug doesn't make sense because I don't know of any case using the delegatecall opcode to call onERC721Received makes sense.

Did someone actually know or verify the "circumstances" that contracts will inadvertently return true, besides the inapplicable DelegateCallReturnValue bug?

Edit:
I thought of two use cases where onERC721Received could be called with delegatecall:

  1. It could be used in a proxy contract.
  2. It could be used in a fallback function as in the example given here: https://ethereum.stackexchange.com/questions/37601/using-a-high-level-delegate-call-in-upgradable-contracts-since-byzantium
@sevriugin

This comment has been minimized.

Show comment
Hide comment
@sevriugin

sevriugin commented Jul 13, 2018

@James-Sangalli

This comment has been minimized.

Show comment
Hide comment
@James-Sangalli

James-Sangalli Jul 17, 2018

Contributor

@dete I cannot understand why bulk transfers are not default or why when querying for a balance you must provide the tokenId instead of the address and returning the array. I understand that you say people can create their own functions on top but it seems to be setting a very bad example and many others will follow suit with the mandatory implementation instead of getting it right from the foundation.

Correct me if I am wrong; but I am yet to see a valid reason why transferring one by one is more logical or simpler to implement? It just seems more painful and wasteful. If you want to transfer only one, then you simply put one element in the array, what's hard about that?

The same reasoning is problematic for querying balance, in cryptokitties you must iterate over every kitty to get the balance of one address, instead of mapping an address to an array of kitties, what is the reasoning behind this?

Contributor

James-Sangalli commented Jul 17, 2018

@dete I cannot understand why bulk transfers are not default or why when querying for a balance you must provide the tokenId instead of the address and returning the array. I understand that you say people can create their own functions on top but it seems to be setting a very bad example and many others will follow suit with the mandatory implementation instead of getting it right from the foundation.

Correct me if I am wrong; but I am yet to see a valid reason why transferring one by one is more logical or simpler to implement? It just seems more painful and wasteful. If you want to transfer only one, then you simply put one element in the array, what's hard about that?

The same reasoning is problematic for querying balance, in cryptokitties you must iterate over every kitty to get the balance of one address, instead of mapping an address to an array of kitties, what is the reasoning behind this?

@ImAllInNow

This comment has been minimized.

Show comment
Hide comment
@ImAllInNow

ImAllInNow Jul 17, 2018

@James-Sangalli, for ERC721s where people build up large collections (i.e. CryptoKitties) returning or operating on arrays of items can quickly become untenable due to gas costs and restrictions.

Do you have a specific implementation of your ideas that show reasonable gas costs at scale?

Creating and modifying an ever increasing array of items in Solidity, if not done correctly, will quickly cost way too much gas. Mappings are much better but require other aspects such as querying to take much longer (which is ok in general since those don't require gas). They do cause your dApp to need to interact with a full node rather than MetaMask because even those queries (i.e. the getAllOwnedKitties function) will timeout when the total kitty array is too large. This is why the ERC721 implementation has the concept of getTokenByIndex to allow the client-side to iterate over the number of tokens owned to fetch all of them 1 at a time.

I do agree that transferring all owned tokens in one transaction is an important use case that needs to be able to be done in one transaction, but also not cost O(n) in gas on the number of tokens owned. I've been throwing around the idea of an add on to ERC-721s that creates an alias between owner address and the owner key that is used in the tokensByOwner mapping. Thus allowing someone to simply allow the alias to change to the new owner, thereby transferring their whole collection. I haven't delved fully into the implementation and security challenges yet, though.

ImAllInNow commented Jul 17, 2018

@James-Sangalli, for ERC721s where people build up large collections (i.e. CryptoKitties) returning or operating on arrays of items can quickly become untenable due to gas costs and restrictions.

Do you have a specific implementation of your ideas that show reasonable gas costs at scale?

Creating and modifying an ever increasing array of items in Solidity, if not done correctly, will quickly cost way too much gas. Mappings are much better but require other aspects such as querying to take much longer (which is ok in general since those don't require gas). They do cause your dApp to need to interact with a full node rather than MetaMask because even those queries (i.e. the getAllOwnedKitties function) will timeout when the total kitty array is too large. This is why the ERC721 implementation has the concept of getTokenByIndex to allow the client-side to iterate over the number of tokens owned to fetch all of them 1 at a time.

I do agree that transferring all owned tokens in one transaction is an important use case that needs to be able to be done in one transaction, but also not cost O(n) in gas on the number of tokens owned. I've been throwing around the idea of an add on to ERC-721s that creates an alias between owner address and the owner key that is used in the tokensByOwner mapping. Thus allowing someone to simply allow the alias to change to the new owner, thereby transferring their whole collection. I haven't delved fully into the implementation and security challenges yet, though.

@James-Sangalli

This comment has been minimized.

Show comment
Hide comment
@James-Sangalli

James-Sangalli Jul 17, 2018

Contributor

@ImAllInNow Thanks for your reply.

If you have a mapping(address => uint256[]) this problem is removed, the likelihood of one address holding a gigantic heap is small and even if someone did, it is still more efficient then querying every single token in the contract to find the ones owned by a single address (e.g. Gigantic holder + everyone else). A mapping like this can easily be called from metamask for 100's of tokens (have tested this with the ERC875 spec when doing a FIFA world cup experiment)

I can see how singular transfer can prevent issues like running out of gas (if you attach a huge array this can happen); however, placing a limit of transfers still seems more sound then defaulting to one at a time.

I know the optional functions can cover this, but my worry is that by creating a bad default you make it very easy for people to simply follow the standard and do things inefficiently.

Contributor

James-Sangalli commented Jul 17, 2018

@ImAllInNow Thanks for your reply.

If you have a mapping(address => uint256[]) this problem is removed, the likelihood of one address holding a gigantic heap is small and even if someone did, it is still more efficient then querying every single token in the contract to find the ones owned by a single address (e.g. Gigantic holder + everyone else). A mapping like this can easily be called from metamask for 100's of tokens (have tested this with the ERC875 spec when doing a FIFA world cup experiment)

I can see how singular transfer can prevent issues like running out of gas (if you attach a huge array this can happen); however, placing a limit of transfers still seems more sound then defaulting to one at a time.

I know the optional functions can cover this, but my worry is that by creating a bad default you make it very easy for people to simply follow the standard and do things inefficiently.

@fulldecent

This comment has been minimized.

Show comment
Hide comment
@fulldecent

fulldecent Jul 17, 2018

Contributor

The architectural discussion in issue #721 is closed.

If you would like to ask a question please go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721

If you have a complaint/idea, please reformulate into a question with a specific use case, then go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721

If you would like to hire a consultant, call me. Or call most anybody else here, plenty of people can help you.

If you would like to make an extension to ERC-721, please start a discussion at -> https://ethereum-magicians.org

Contributor

fulldecent commented Jul 17, 2018

The architectural discussion in issue #721 is closed.

If you would like to ask a question please go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721

If you have a complaint/idea, please reformulate into a question with a specific use case, then go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721

If you would like to hire a consultant, call me. Or call most anybody else here, plenty of people can help you.

If you would like to make an extension to ERC-721, please start a discussion at -> https://ethereum-magicians.org

@James-Sangalli

This comment has been minimized.

Show comment
Hide comment
@fulldecent

This comment has been minimized.

Show comment
Hide comment
@fulldecent

fulldecent Aug 16, 2018

Contributor

I have read the article. And like I said, "If you have a complaint/idea, please reformulate into a question with a specific use case, then go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721".

There is nothing in the article that motivates me to change that stance.

Contributor

fulldecent commented Aug 16, 2018

I have read the article. And like I said, "If you have a complaint/idea, please reformulate into a question with a specific use case, then go to -> https://ethereum.stackexchange.com/questions/tagged/erc-721".

There is nothing in the article that motivates me to change that stance.

@wighawag

This comment has been minimized.

Show comment
Hide comment
@wighawag

wighawag Sep 18, 2018

Contributor

Hi,
I know the EIP has been approved but I would like to discuss a potential beneficial change :

Currently as defined, the Transfer event is not required to be emitted when the contract is being created and assign ownership:

Quoting the EIP:

Exception: during contract creation, any number of NFTs may be created and assigned without emitting Transfer

I suppose this is that way to allow previous implementation to be compatible with the standard.

In my opinion this is a mistake since if Transfer was mandatory even in that case, we could track ownership simply by tracing the history of all Transfer event.

Without it is is not possible to create map of ownership without extra parsing (which might require contract specific parsing) unless they support the 'enumeration extension'

Contributor

wighawag commented Sep 18, 2018

Hi,
I know the EIP has been approved but I would like to discuss a potential beneficial change :

Currently as defined, the Transfer event is not required to be emitted when the contract is being created and assign ownership:

Quoting the EIP:

Exception: during contract creation, any number of NFTs may be created and assigned without emitting Transfer

I suppose this is that way to allow previous implementation to be compatible with the standard.

In my opinion this is a mistake since if Transfer was mandatory even in that case, we could track ownership simply by tracing the history of all Transfer event.

Without it is is not possible to create map of ownership without extra parsing (which might require contract specific parsing) unless they support the 'enumeration extension'

@DanielRX

This comment has been minimized.

Show comment
Hide comment
@DanielRX

DanielRX Sep 18, 2018

@wighawag Please follow the notes on this thread and go to the links seen in this post just up the page

DanielRX commented Sep 18, 2018

@wighawag Please follow the notes on this thread and go to the links seen in this post just up the page

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