Skip to content

Latest commit

 

History

History
459 lines (374 loc) · 18.7 KB

nep-0171.md

File metadata and controls

459 lines (374 loc) · 18.7 KB
NEP Title Author DiscussionsTo Status Type Category Version Created Updated Requires
171
Non Fungible Token Standard
Mike Purvis <mike@near.org>, Evgeny Kuzyakov <ek@near.org>, @oysterpack
Final
Standards Track
Contract
1.2.0
03-Mar-2022
13-Mar-2023
297

Summary

A standard interface for non-fungible tokens (NFTs). That is, tokens which each have a unique ID.

Motivation

In the three years since ERC-721 was ratified by the Ethereum community, Non-Fungible Tokens have proven themselves as an incredible new opportunity across a wide array of disciplines: collectibles, art, gaming, finance, virtual reality, real estate, and more.

This standard builds off the lessons learned in this early experimentation, and pushes the possibilities further by harnessing unique attributes of the NEAR blockchain:

  • an asynchronous, sharded runtime, meaning that two contracts can be executed at the same time in different shards
  • a storage staking model that separates gas fees from the storage demands of the network, enabling greater on-chain storage (see Metadata extension) and ultra-low transaction fees

Given these attributes, this NFT standard can accomplish with one user interaction things for which other blockchains need two or three. Most noteworthy is nft_transfer_call, by which a user can essentially attach a token to a call to a separate contract. An example scenario:

  • An Exquisite Corpse contract allows three drawings to be submitted, one for each section of a final composition, to be minted as its own NFT and sold on a marketplace, splitting royalties amongst the original artists.
  • Alice draws the top third and submits it, Bob the middle third, and Carol follows up with the bottom third. Since they each use nft_transfer_call to both transfer their NFT to the Exquisite Corpse contract as well as call a submit method on it, the call from Carol can automatically kick off minting a composite NFT from the three submissions, as well as listing this composite NFT in a marketplace.
  • When Dan attempts to also call nft_transfer_call to submit an unneeded top third of the drawing, the Exquisite Corpse contract can throw an error, and the transfer will be rolled back so that Bob maintains ownership of his NFT.

While this is already flexible and powerful enough to handle all sorts of existing and new use-cases, apps such as marketplaces may still benefit from the Approval Management extension.

Prior art:

Rationale and alternatives

  • Why is this design the best in the space of possible designs?
  • What other designs have been considered and what is the rationale for not choosing them?
  • What is the impact of not doing this?

Specification

NOTES:

  • All amounts, balances and allowance are limited by U128 (max value 2**128 - 1).
  • Token standard uses JSON for serialization of arguments and results.
  • Amounts in arguments and results are serialized as Base-10 strings, e.g. "100". This is done to avoid JSON limitation of max integer value of 2**53.
  • The contract must track the change in storage when adding to and removing from collections. This is not included in this core fungible token standard but instead in the Storage Standard.
  • To prevent the deployed contract from being modified or deleted, it should not have any access keys on its account.

NFT Interface

// The base structure that will be returned for a token. If contract is using
// extensions such as Approval Management, Metadata, or other
// attributes may be included in this structure.
type Token = {
   token_id: string,
   owner_id: string,
 }

/******************/
/* CHANGE METHODS */
/******************/

// Simple transfer. Transfer a given `token_id` from current owner to
// `receiver_id`.
//
// Requirements
// * Caller of the method must attach a deposit of 1 yoctoⓃ for security purposes
// * Contract MUST panic if called by someone other than token owner or,
//   if using Approval Management, one of the approved accounts
// * `approval_id` is for use with Approval Management extension, see
//   that document for full explanation.
// * If using Approval Management, contract MUST nullify approved accounts on
//   successful transfer.
//
// Arguments:
// * `receiver_id`: the valid NEAR account receiving the token
// * `token_id`: the token to transfer
// * `approval_id`: expected approval ID. A number smaller than
//    2^53, and therefore representable as JSON. See Approval Management
//    standard for full explanation.
// * `memo` (optional): for use cases that may benefit from indexing or
//    providing information for a transfer
function nft_transfer(
  receiver_id: string,
  token_id: string,
  approval_id: number|null,
  memo: string|null,
) {}

// Returns `true` if the token was transferred from the sender's account.

// Transfer token and call a method on a receiver contract. A successful
// workflow will end in a success execution outcome to the callback on the NFT
// contract at the method `nft_resolve_transfer`.
//
// You can think of this as being similar to attaching native NEAR tokens to a
// function call. It allows you to attach any Non-Fungible Token in a call to a
// receiver contract.
//
// Requirements:
// * Caller of the method must attach a deposit of 1 yoctoⓃ for security
//   purposes
// * Contract MUST panic if called by someone other than token owner or,
//   if using Approval Management, one of the approved accounts
// * The receiving contract must implement `nft_on_transfer` according to the
//   standard. If it does not, FT contract's `nft_resolve_transfer` MUST deal
//   with the resulting failed cross-contract call and roll back the transfer.
// * Contract MUST implement the behavior described in `nft_resolve_transfer`
// * `approval_id` is for use with Approval Management extension, see
//   that document for full explanation.
// * If using Approval Management, contract MUST nullify approved accounts on
//   successful transfer.
//
// Arguments:
// * `receiver_id`: the valid NEAR account receiving the token.
// * `token_id`: the token to send.
// * `approval_id`: expected approval ID. A number smaller than
//    2^53, and therefore representable as JSON. See Approval Management
//    standard for full explanation.
// * `memo` (optional): for use cases that may benefit from indexing or
//    providing information for a transfer.
// * `msg`: specifies information needed by the receiving contract in
//    order to properly handle the transfer. Can indicate both a function to
//    call and the parameters to pass to that function.
function nft_transfer_call(
  receiver_id: string,
  token_id: string,
  approval_id: number|null,
  memo: string|null,
  msg: string,
): Promise {}


/****************/
/* VIEW METHODS */
/****************/

// Returns the token with the given `token_id` or `null` if no such token.
function nft_token(token_id: string): Token|null {}

The following behavior is required, but contract authors may name this function something other than the conventional nft_resolve_transfer used here.

// Finalize an `nft_transfer_call` chain of cross-contract calls.
//
// The `nft_transfer_call` process:
//
// 1. Sender calls `nft_transfer_call` on NFT contract
// 2. NFT contract transfers token from sender to receiver
// 3. NFT contract calls `nft_on_transfer` on receiver contract
// 4+. [receiver contract may make other cross-contract calls]
// N. NFT contract resolves promise chain with `nft_resolve_transfer`, and may
//    transfer token back to sender
//
// Requirements:
// * Contract MUST forbid calls to this function by any account except self
// * If promise chain failed, contract MUST revert token transfer
// * If promise chain resolves with `true`, contract MUST return token to
//   `owner_id`
//
// Arguments:
// * `owner_id`: the original owner of the NFT.
// * `receiver_id`: the `receiver_id` argument given to `nft_transfer_call`
// * `token_id`: the `token_id` argument given to `nft_transfer_call`
// * `approved_account_ids `: if using Approval Management, contract MUST provide
//   record of original approved accounts in this argument, and restore these
//   approved accounts and their approval IDs in case of revert.
//
// Returns true if token was successfully transferred to `receiver_id`.
function nft_resolve_transfer(
  owner_id: string,
  receiver_id: string,
  token_id: string,
  approved_account_ids: null|Record<string, number>,
): boolean {}

Receiver Interface

Contracts which want to make use of nft_transfer_call must implement the following:

// Take some action after receiving a non-fungible token
//
// Requirements:
// * Contract MUST restrict calls to this function to a set of whitelisted NFT
//   contracts
//
// Arguments:
// * `sender_id`: the sender of `nft_transfer_call`
// * `previous_owner_id`: the account that owned the NFT prior to it being
//   transferred to this contract, which can differ from `sender_id` if using
//   Approval Management extension
// * `token_id`: the `token_id` argument given to `nft_transfer_call`
// * `msg`: information necessary for this contract to know how to process the
//   request. This may include method names and/or arguments.
//
// Returns true if token should be returned to `sender_id`
function nft_on_transfer(
  sender_id: string,
  previous_owner_id: string,
  token_id: string,
  msg: string,
): Promise<boolean>;

Events

NEAR and third-party applications need to track mint, transfer, burn, metadata update, and contract metadata update events for all NFT-driven apps consistently. This extension addresses that.

Keep in mind that applications, including NEAR Wallet, could require implementing additional methods to display the NFTs correctly, such as nft_metadata and nft_tokens_for_owner).

Events Interface

Non-Fungible Token Events MUST have standard set to "nep171", standard version set to "1.2.0", event value is one of nft_mint, nft_burn, nft_transfer, nft_metadata_update, or contract_metadata_update, and data must be of one of the following relevant types: NftMintLog[] | NftTransferLog[] | NftBurnLog[] | NftMetadataUpdateLog[] | NftContractMetadataUpdateLog[]:

interface NftEventLogData {
    standard: "nep171",
    version: "1.2.0",
    event: "nft_mint" | "nft_burn" | "nft_transfer" | "nft_metadata_update" | "contract_metadata_update",
    data: NftMintLog[] | NftTransferLog[] | NftBurnLog[] | NftMetadataUpdateLog[] | NftContractMetadataUpdateLog[],
}
// An event log to capture token minting
// Arguments
// * `owner_id`: "account.near"
// * `token_ids`: ["1", "abc"]
// * `memo`: optional message
interface NftMintLog {
    owner_id: string,
    token_ids: string[],
    memo?: string
}

// An event log to capture token burning
// Arguments
// * `owner_id`: owner of tokens to burn
// * `authorized_id`: approved account_id to burn, if applicable
// * `token_ids`: ["1","2"]
// * `memo`: optional message
interface NftBurnLog {
    owner_id: string,
    authorized_id?: string,
    token_ids: string[],
    memo?: string
}

// An event log to capture token transfer
// Arguments
// * `authorized_id`: approved account_id to transfer, if applicable
// * `old_owner_id`: "owner.near"
// * `new_owner_id`: "receiver.near"
// * `token_ids`: ["1", "12345abc"]
// * `memo`: optional message
interface NftTransferLog {
    authorized_id?: string,
    old_owner_id: string,
    new_owner_id: string,
    token_ids: string[],
    memo?: string
}

// An event log to capture token metadata updating
// Arguments
// * `token_ids`: ["1", "abc"]
// * `memo`: optional message
interface NftMetadataUpdateLog {
    token_ids: string[],
    memo?: string
}

// An event log to capture contract metadata updates. Note that the updated contract metadata is not included in the log, as it could easily exceed the 16KB log size limit. Listeners can query `nft_metadata` to get the updated contract metadata.
// Arguments
// * `memo`: optional message
interface NftContractMetadataUpdateLog {
    memo?: string
}

Examples

Single owner batch minting (pretty-formatted for readability purposes):

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_mint",
  "data": [
    {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs"]}
  ]
}

Different owners batch minting:

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_mint",
  "data": [
    {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs"]},
    {"owner_id": "user1.near", "token_ids": ["meme"]}
  ]
}

Different events (separate log entries):

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_burn",
  "data": [
    {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs"]},
  ]
}
EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_transfer",
  "data": [
    {"old_owner_id": "user1.near", "new_owner_id": "user2.near", "token_ids": ["meme"], "memo": "have fun!"}
  ]
}

Authorized id:

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_burn",
  "data": [
    {"owner_id": "owner.near", "token_ids": ["goodbye", "aurevoir"], "authorized_id": "thirdparty.near"}
  ]
}

NFT Metadata Update:

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "nft_metadata_update",
  "data": [
    {"token_ids": ["1", "2"]}
  ]
}

Contract metadata update:

EVENT_JSON:{
  "standard": "nep171",
  "version": "1.2.0",
  "event": "contract_metadata_update",
  "data": []
}

Events for Other NFT Methods

Note that the example events above cover two different kinds of events:

  1. Events that do not have a dedicated trigger function in the NFT Standard (nft_mint, nft_metadata_update, nft_burn, contract_metadata_update)
  2. An event that has a relevant trigger function NFT Core Standard (nft_transfer)

This event standard also applies beyond the events highlighted here, where future events follow the same convention of as the second type. For instance, if an NFT contract uses the approval management standard, it may emit an event for nft_approve if that's deemed as important by the developer community.

Please feel free to open pull requests for extending the events standard detailed here as needs arise.

Reference Implementation

Minimum Viable Interface

NFT Implementation

Changelog

1.0.0 - Initial version

This NEP had several pre-1.0.0 iterations that led to the following errata updates to this NEP:

  • 2022-02-03: updated Token struct field names. id was changed to token_id. This is to be consistent with current implementations of the standard and the rust SDK docs.
  • 2021-12-20: updated nft_resolve_transfer argument approved_account_ids to be type null|Record<string, number> instead of null|string[]. This gives contracts a way to restore the original approved accounts and their approval IDs. More information can be found in this discussion.
  • 2021-07-16: updated nft_transfer_call argument approval_id to be type number|null instead of string|null. As stated, approval IDs are not expected to exceed the JSON limit of 2^53.

1.1.0 - Add contract_metadata_update Event

The extension NEP-0423 that added Contract Metadata Update Event Kind to this NEP-0171 was approved by Contract Standards Working Group members (@frol, @abacabadabacaba, @mfornet) on January 13, 2023 (meeting recording).

Benefits

  • This new event type will help indexers to invalidate their cached values reliably and efficiently
  • This NEP extension only introduces an additional event type, so there is no breaking change to the original NEP

Concerns

# Concern Resolution Status
1 Old NFT contracts do not emit JSON Events at all; more recent NFT contracts will only emit mint/burn/transfer events, so when it comes to legacy contracts support, we won’t benefit from this new event type and only further fragment the implementations Legacy contracts usage will die out eventually and new contracts will support new features in a non-breaking way Resolved
2 There is a need to have a similar event type for individual NFT updates It is outside of the scope of this NEP extension. Feel free to create a follow-up proposal Resolved

1.2.0 - Add nft_metadata_update Event

The extension NEP-0469 that added Token Metadata Update Event Kind to this NEP-0171 was approved by Contract Standards Working Group members (@frol, @abacabadabacaba, @mfornet, @fadeevab, @robert-zaremba) on April 21, 2023 (meeting recording).

Benefits

  • Apps that cache indexed NFTs will benefit from having this new event type as they won't need to have a custom logic to track potentially changing NFTs or refetch all NFTs on every transaction to NFT contract
  • It plays well with contract_metadata_update event that was introduced in version 1.1.0 of this NEP
  • This NEP extension only introduces an additional event type, so there is no breaking change to the original NEP

Concerns

# Concern Resolution Status
1 Ecosystem will be split where legacy contracts won't emit these new events, so legacy support will still be needed In the future, there will be fewer legacy contracts and eventually apps will have support for this type of event Resolved
2 nft_update event name is ambiguous It was decided to use nft_metadata_update name, instead Resolved

Copyright

Copyright and related rights waived via CC0.