diff --git a/README.md b/README.md index 9ca5a1f6f..9772014ef 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Changes to the protocol specification and standards are called NEAR Enhancement |[0178](https://github.com/near/NEPs/blob/master/neps/nep-0178.md) | Non Fungible Token Approval Management | @chadoh @thor314 | Final | |[0181](https://github.com/near/NEPs/blob/master/neps/nep-0181.md) | Non Fungible Token Enumeration | @chadoh @thor314 | Final | |[0199](https://github.com/near/NEPs/blob/master/neps/nep-0199.md) | Non Fungible Token Royalties and Payouts | @thor314 @mattlockyer | Final | +|[0245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) | Multi Token Standard | @zcstarr @riqi @jriemann @marcos.sun | Draft | |[0297](https://github.com/near/NEPs/blob/master/neps/nep-0297.md) | Contract Events Standard | @telezhnaya | Final | |[0330](https://github.com/near/NEPs/blob/master/neps/nep-0330.md) | Contract Metadata | @BenKurrek | Review | diff --git a/neps/nep-0245.md b/neps/nep-0245.md new file mode 100644 index 000000000..a9247bd3b --- /dev/null +++ b/neps/nep-0245.md @@ -0,0 +1,583 @@ +--- +NEP: 245 +Title: Multi Token Standard +Author: Zane Starr , @riqi, @jriemann, @marcos.sun +DiscussionsTo: https://github.com/near/NEPs/discussions/246 +Status: Draft +Type: Standards Track +Category: Contract +Created: 03-Mar-2022 +Requires: 297 +--- + +## Summary + +A standard interface for a multi token standard that supports fungible, semi-fungible,non-fungible, and tokens of any type, allowing for ownership, transfer, and batch transfer of tokens regardless of specific type. + +## Motivation + + +In the three years since [ERC-1155] was ratified by the Ethereum Community, Multi Token based contracts have proven themselves valuable assets. Many blockchain projects emulate this standard for representing multiple token assets classes in a single contract. The ability to reduce transaction overhead for marketplaces, video games, DAOs, and exchanges is appealing to the blockchain ecosystem and simplifies transactions for developers. + +Having a single contract represent NFTs, FTs, and tokens that sit inbetween greatly improves efficiency. The standard also introduced the ability to make batch requests with multiple asset classes reducing complexity. This standard allows operations that currently require _many_ transactions to be completed in a single transaction that can transfer not only NFTs and FTs, but any tokens that are a part of same token contract. + +With this standard, we have sought to take advantage of the ability of the NEAR blockchain to scale. Its sharded runtime, and [storage staking] model that decouples [gas] fees from storage demand, enables ultra low transaction fees and greater on chain storage ( see [Metadata] extension). + +With the aforementioned, it is noteworthy to mention that like the [NFT] standard the Multi Token standard, implements `mt_transfer_call`, +which allows, a user to attach many tokens to a call to a separate contract. Additionally, this standard includes an optional [Approval Management] extension. The extension allows marketplaces to trade on behalf of a user, providing additional flexibility for dApps. + +Prior art: + +- [ERC-721] +- [ERC-1155] +- [NEAR Fungible Token Standard][FT], which first pioneered the "transfer and call" technique +- [NEAR Non-Fungible Token Standard][NFT] + +## Rationale and alternatives + +Why have another standard, aren't fungible and non-fungible tokens enough? The current fungible token and non-fungible token standards, do not provide support for representing many FT tokens in a single contract, as well as the flexibility to define different token types with different behavior in a single contract. This is something that makes it difficult to be interoperable with other major blockchain networks, that implement standards that allow for representation of many different FT tokens in a single contract such as Ethereum. + +The standard here introduces a few concepts that evolve the original [ERC-1155] standard to have more utility, while maintaining the original flexibility of the standard. So keeping that in mind, we are defining this as a new token type. It combines two main features of FT and NFT. It allows us to represent many token types in a single contract, and it's possible to store the amount for each token. + +The decision to not use FT and NFT as explicit token types was taken to allow the community to define their own standards and meanings through metadata. As standards evolve on other networks, this specification allows the standard to be able to represent tokens across networks accurately, without necessarily restricting the behavior to any preset definition. + +The issues with this in general is a problem with defining what metadata means and how is that interpreted. We have chosen to follow the pattern that is currently in use on Ethereum in the [ERC-1155] standard. That pattern relies on people to make extensions or to make signals as to how they want the metadata to be represented for their use case. + +One of the areas that has broad sweeping implications from the [ERC-1155] standard is the lack of direct access to metadata. With Near's sharding we are able to have a [Metadata Extension](Metadata.md) for the standard that exists on chain. So developers and users are not required to use an indexer to understand, how to interact or interpret tokens, via token identifiers that they receive. + +Another extension that we made was to provide an explicit ability for developers and users to group or link together series of NFTs/FTs or any combination of tokens. This provides additional flexiblity that the [ERC-1155] standard only has loose guidelines on. This was chosen to make it easy for consumers to understand the relationship between tokens within the contract. + +To recap, we choose to create this standard, to improve interoperability, developer ease of use, and to extend token representability beyond what was available directly in the FT or NFT standards. We believe this to be another tool in the developer's toolkit. It makes it possible to represent many types of tokens and to enable exchanges of many tokens within a single `transaction`. + +## 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 multi token standard but instead in the [Storage Standard](../StorageManagement.md). +- To prevent the deployed contract from being modified or deleted, it should not have any access keys on its account. + +### MT Interface + +```ts +// The base structure that will be returned for a token. If contract is using +// extensions such as Approval Management, Enumeration, Metadata, or other +// attributes may be included in this structure. +type Token = { + token_id: string, + owner_id: string | null +} + +/******************/ +/* 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 +// * Caller must have greater than or equal to the `amount` being requested +// * 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 +// * `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// * `approval` (optional): is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_transfer( + receiver_id: string, + token_id: string, + amount: string, + approval: [owner_id: string, approval_id: number]|null, + memo: string|null, +) {} + +// Simple batch transfer. Transfer a given `token_ids` from current owner to +// `receiver_id`. +// +// Requirements +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security purposes +// * Caller must have greater than or equal to the `amounts` being requested for the given `token_ids` +// * 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. +// * Contract MUST panic if called with the length of `token_ids` not equal to `amounts` is not equal +// * Contract MUST panic if `approval_ids` is not `null` and does not equal the length of `token_ids` +// +// Arguments: +// * `receiver_id`: the valid NEAR account receiving the token +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like an array of strings, although the numbers will be stored as an array of unsigned integer +// with 128 bits. +// * `approvals` (optional): is an array of expected `approval` per `token_ids`. +// If a `token_id` does not have a corresponding `approval` then the entry in the array +// must be marked null. +// `approval` is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_batch_transfer( + receiver_id: string, + token_ids: string[], + amounts: string[], + approvals: ([owner_id: string, approval_id: number]| null)[]| null, + memo: string|null, +) {} + + +// 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 MT +// contract at the method `mt_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 Multi Token, token in a call to a +// receiver contract. +// +// Requirements: +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * Caller must have greater than or equal to the `amount` being requested +// * 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 `mt_on_transfer` according to the +// standard. If it does not, MT contract's `mt_resolve_transfer` MUST deal +// with the resulting failed cross-contract call and roll back the transfer. +// * Contract MUST implement the behavior described in `mt_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. +// * `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// * `owner_id`: the valid NEAR account that owns the token +// * `approval` (optional): is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the expected approval ID. A number smaller than +// 2^53, and therefore representable as JSON. See Approval Management +// * `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 mt_transfer_call( + receiver_id: string, + token_id: string, + amount: string, + approval: [owner_id: string, approval_id: number]|null, + memo: string|null, + msg: string, +): Promise {} + + + +// Transfer tokens and call a method on a receiver contract. A successful +// workflow will end in a success execution outcome to the callback on the MT +// contract at the method `mt_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 Multi Token, token in a call to a +// receiver contract. +// +// Requirements: +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * Caller must have greater than or equal to the `amount` being requested +// * 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 `mt_on_transfer` according to the +// standard. If it does not, MT contract's `mt_resolve_transfer` MUST deal +// with the resulting failed cross-contract call and roll back the transfer. +// * Contract MUST implement the behavior described in `mt_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. +// * Contract MUST panic if called with the length of `token_ids` not equal to `amounts` is not equal +// * Contract MUST panic if `approval_ids` is not `null` and does not equal the length of `token_ids` +// +// Arguments: +// * `receiver_id`: the valid NEAR account receiving the token. +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like an array of string, although the numbers will be stored as an array of +// unsigned integer with 128 bits. +// * `approvals` (optional): is an array of expected `approval` per `token_ids`. +// If a `token_id` does not have a corresponding `approval` then the entry in the array +// must be marked null. +// `approval` is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_batch_transfer_call( + receiver_id: string, + token_ids: string[], + amounts: string[], + approvals: ([owner_id: string, approval_id: number]|null)[] | null, + memo: string|null, + msg: string, +): Promise {} + +/****************/ +/* VIEW METHODS */ +/****************/ + + +// Returns the tokens with the given `token_ids` or `null` if no such token. +function mt_token(token_ids: string[]) (Token | null)[] + +// Returns the balance of an account for the given `token_id`. +// The balance though wrapped in quotes and treated like a string, +// the number will be stored as an unsigned integer with 128 bits. +// Arguments: +// * `account_id`: the NEAR account that owns the token. +// * `token_id`: the token to retrieve the balance from +function mt_balance_of(account_id: string, token_id: string): string + +// Returns the balances of an account for the given `token_ids`. +// The balances though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +// Arguments: +// * `account_id`: the NEAR account that owns the tokens. +// * `token_ids`: the tokens to retrieve the balance from +function mt_batch_balance_of(account_id: string, token_ids: string[]): string[] + +// Returns the token supply with the given `token_id` or `null` if no such token exists. +// The supply though wrapped in quotes and treated like a string, the number will be stored +// as an unsigned integer with 128 bits. +function mt_supply(token_id: string): string | null + +// Returns the token supplies with the given `token_ids`, a string value is returned or `null` +// if no such token exists. The supplies though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +function mt_batch_supply(token_ids: string[]): (string | null)[] +``` + +The following behavior is required, but contract authors may name this function something other than the conventional `mt_resolve_transfer` used here. + +```ts +// Finalize an `mt_transfer_call` or `mt_batch_transfer_call` chain of cross-contract calls. Generically +// referred to as `mt_transfer_call` as it applies to `mt_batch_transfer_call` as well. +// +// The `mt_transfer_call` process: +// +// 1. Sender calls `mt_transfer_call` on MT contract +// 2. MT contract transfers token from sender to receiver +// 3. MT contract calls `mt_on_transfer` on receiver contract +// 4+. [receiver contract may make other cross-contract calls] +// N. MT contract resolves promise chain with `mt_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 +// `sender_id` +// +// Arguments: +// * `sender_id`: the sender of `mt_transfer_call` +// * `receiver_id`: the `receiver_id` argument given to `mt_transfer_call` +// * `token_ids`: the `token_ids` argument given to `mt_transfer_call` +// * `amounts`: the `token_ids` argument given to `mt_transfer_call` +// * `approvals (optional)`: if using Approval Management, contract MUST provide +// set of original approvals in this argument, and restore the +// approved accounts in case of revert. +// `approvals` is an array of expected `approval_list` per `token_ids`. +// If a `token_id` does not have a corresponding `approvals_list` then the entry in the +// array must be marked null. +// `approvals_list` is an array of triplets of [`owner_id`,`approval_id`,`amount`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the expected approval ID. A number smaller than +// 2^53, and therefore representable as JSON. See Approval Management +// standard for full explanation. +// `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// +// +// +// Returns total amount spent by the `receiver_id`, corresponding to the `token_id`. +// The amounts returned, though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +// Example: if sender_id calls `mt_transfer_call({ "amounts": ["100"], token_ids: ["55"], receiver_id: "games" })`, +// but `receiver_id` only uses 80, `mt_on_transfer` will resolve with `["20"]`, and `mt_resolve_transfer` +// will return `["80"]`. + + +function mt_resolve_transfer( + sender_id: string, + receiver_id: string, + token_ids: string[], + approvals: (null | [owner_id: string, approval_id: number, amount: string][]) []| null +):string[] {} +``` + +### Receiver Interface + +Contracts which want to make use of `mt_transfer_call` and `mt_batch_transfer_call` must implement the following: + +```ts +// Take some action after receiving a multi token +// +// Requirements: +// * Contract MUST restrict calls to this function to a set of whitelisted +// contracts +// * Contract MUST panic if `token_ids` length does not equals `amounts` +// length +// * Contract MUST panic if `previous_owner_ids` length does not equals `token_ids` +// length +// +// Arguments: +// * `sender_id`: the sender of `mt_transfer_call` +// * `previous_owner_ids`: the account that owned the tokens prior to it being +// transferred to this contract, which can differ from `sender_id` if using +// Approval Management extension +// * `token_ids`: the `token_ids` argument given to `mt_transfer_call` +// * `amounts`: the `token_ids` argument given to `mt_transfer_call` +// * `msg`: information necessary for this contract to know how to process the +// request. This may include method names and/or arguments. +// +// Returns the number of unused tokens in string form. For instance, if `amounts` +// is `["10"]` but only 9 are needed, it will return `["1"]`. The amounts returned, +// though wrapped in quotes and treated like strings, the numbers will be stored as +// an unsigned integer with 128 bits. + + +function mt_on_transfer( + sender_id: string, + previous_owner_ids: string[], + token_ids: string[], + amounts: string[], + msg: string, +): Promise; +``` + +## Events + +NEAR and third-party applications need to track + `mint`, `burn`, `transfer` events for all MT-driven apps consistently. This exension addresses that. + +Note that applications, including NEAR Wallet, could require implementing additional methods to display tokens correctly such as [`mt_metadata`](Metadata.md) and [`mt_tokens_for_owner`](Enumeration.md). + +### Events Interface +Multi Token Events MUST have `standard` set to `"nep245"`, standard version set to `"1.0.0"`, `event` value is one of `mt_mint`, `mt_burn`, `mt_transfer`, and `data` must be of one of the following relavant types: `MtMintLog[] | MtBurnLog[] | MtTransferLog[]`: + + + +```ts +interface MtEventLogData { + EVENT_JSON: { + standard: "nep245", + version: "1.0.0", + event: MtEvent, + data: MtMintLog[] | MtBurnLog[] | MtTransferLog[] + } +} +``` + +```ts +// Minting event log. Emitted when a token is minted/created. +// Requirements +// * Contract MUST emit event when minting a token +// Fields +// * Contract token_ids and amounts MUST be the same length +// * `owner_id`: the account receiving the minted token +// * `token_ids`: the tokens minted +// * `amounts`: the number of tokens minted, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +// * `memo`: optional message +interface MtMintLog { + owner_id: string, + token_ids: string[], + amounts: string[], + memo?: string +} + +// Burning event log. Emitted when a token is burned. +// Requirements +// * Contract MUST emit event when minting a token +// Fields +// * Contract token_ids and amounts MUST be the same length +// * `owner_id`: the account whose token(s) are being burned +// * `authorized_id`: approved account_id to burn, if applicable +// * `token_ids`: the tokens being burned +// * `amounts`: the number of tokens burned, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +// * `memo`: optional message +interface MtBurnLog { + owner_id: string, + authorized_id?: string, + token_ids: string[], + amounts: string[], + memo?: string +} + +// Transfer event log. Emitted when a token is transferred. +// Requirements +// * Contract MUST emit event when transferring a token +// Fields +// * `authorized_id`: approved account_id to transfer +// * `old_owner_id`: the account sending the tokens "sender.near" +// * `new_owner_id`: the account receiving the tokens "receiver.near" +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +interface MtTransferLog { + authorized_id?: string, + old_owner_id: string, + new_owner_id: string, + token_ids: string[], + amounts: string[], + memo?: string +} +``` + +## Examples + +Single owner minting (pretty-formatted for readability purposes): + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_mint", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts":["1", "100"]} + ] +} +``` + +Different owners minting: + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_mint", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts":["1","100"]}, + {"owner_id": "user1.near", "token_ids": ["meme"], "amounts": ["1"]} + ] +} +``` + +Different events (separate log entries): + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_burn", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts": ["1","100"]}, + ] +} +``` + +Authorized id: + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_burn", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora_alpha", "proximitylabs_ft"], "amounts": ["1","100"], "authorized_id": "thirdparty.near" }, + ] +} +``` + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_transfer", + "data": [ + {"old_owner_id": "user1.near", "new_owner_id": "user2.near", "token_ids": ["meme"], "amounts":["1"], "memo": "have fun!"} + ] +} + +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_transfer", + "data": [ + {"old_owner_id": "user2.near", "new_owner_id": "user3.near", "token_ids": ["meme"], "amounts":["1"], "authorized_id": "thirdparty.near", "memo": "have fun!"} + ] +} +``` + +## Further Event Methods + +Note that the example events covered above cover two different kinds of events: +1. Events that are not specified in the MT Standard (`mt_mint`, `mt_burn`) +2. An event that is covered in the [Multi Token Core Standard](https://nomicon.io/Standards/MultiToken/Core.html#mt-interface). (`mt_transfer`) + +This event standard also applies beyond the three events highlighted here, where future events follow the same convention of as the second type. For instance, if an MT contract uses the [approval management standard](https://nomicon.io/Standards/MultiToken/ApprovalManagement.html), it may emit an event for `mt_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](https://github.com/jriemann/near-sdk-rs/blob/multi-token-reference-impl/near-contract-standards/src/multi_token/core/mod.rs) + +[MT Implementation](https://github.com/jriemann/near-sdk-rs/blob/multi-token-reference-impl/near-contract-standards/src/multi_token/core/core_impl.rs) + + + +## Copyright +[copyright]: #copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + + [ERC-721]: https://eips.ethereum.org/EIPS/eip-721 + [ERC-1155]: https://eips.ethereum.org/EIPS/eip-1155 + [storage staking]: https://docs.near.org/docs/concepts/storage-staking + [gas]: https://docs.near.org/docs/concepts/gas + [Metadata]: ../specs/Standards/MultiToken/Metadata.md + [NFT]: ../specs/Standards/NonFungibleToken/Core.md + [Approval Management]: ../specs/Standards/MultiToken/ApprovalManagement.md + [FT]: ../specs/Standards/FungibleToken/Core.md \ No newline at end of file diff --git a/specs/Standards/MultiToken/ApprovalManagement.md b/specs/Standards/MultiToken/ApprovalManagement.md new file mode 100644 index 000000000..11420f76e --- /dev/null +++ b/specs/Standards/MultiToken/ApprovalManagement.md @@ -0,0 +1,503 @@ +# Multi Token Standard Approval Management([NEP-245](https://github.com/near/NEPs/discussions/246)) + +:::caution +This is part of the proposed spec [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) and is subject to change. +::: + + +Version `1.0.0` + +## Summary + +A system for allowing a set of users or contracts to transfer specific tokens on behalf of an owner. Similar to approval management systems in standards like [ERC-721] and [ERC-1155]. + + [ERC-721]: https://eips.ethereum.org/EIPS/eip-721 + [ERC-1155]: https://eips.ethereum.org/EIPS/eip-1155 +## Motivation + +People familiar with [ERC-721] may expect to need an approval management system for basic transfers, where a simple transfer from Alice to Bob requires that Alice first _approve_ Bob to spend one of her tokens, after which Bob can call `transfer_from` to actually transfer the token to himself. + +NEAR's [core Multi Token standard](README.md) includes good support for safe atomic transfers without such complexity. It even provides "transfer and call" functionality (`mt_transfer_call`) which allows specific tokens to be "attached" to a call to a separate contract. For many token workflows, these options may circumvent the need for a full-blown Approval Management system. + +However, some Multi Token developers, marketplaces, dApps, or artists may require greater control. This standard provides a uniform interface allowing token owners to approve other NEAR accounts, whether individuals or contracts, to transfer specific tokens on the owner's behalf. + +Prior art: + +- Ethereum's [ERC-721] +- Ethereum's [ERC-1155] + +## Example Scenarios + +Let's consider some examples. Our cast of characters & apps: + +* Alice: has account `alice` with no contract deployed to it +* Bob: has account `bob` with no contract deployed to it +* MT: a contract with account `mt`, implementing only the [Multi Token Standard](Core.md) with this Approval Management extension +* Market: a contract with account `market` which sells tokens from `mt` as well as other token contracts +* Bazaar: similar to Market, but implemented differently (spoiler alert: has no `mt_on_approve` function!), has account `bazaar` + +Alice and Bob are already [registered](../StorageManagement.md) with MT, Market, and Bazaar, and Alice owns a token on the MT contract with ID=`"1"` and a fungible style token with ID =`"2"` and AMOUNT =`"100"`. + +Let's examine the technical calls through the following scenarios: + +1. [Simple approval](#1-simple-approval): Alice approves Bob to transfer her token. +2. [Approval with cross-contract call (XCC)](#2-approval-with-cross-contract-call): Alice approves Market to transfer one of her tokens and passes `msg` so that MT will call `mt_on_approve` on Market's contract. +3. [Approval with XCC, edge case](#3-approval-with-cross-contract-call-edge-case): Alice approves Bazaar and passes `msg` again, but what's this? Bazaar doesn't implement `mt_on_approve`, so Alice sees an error in the transaction result. Not to worry, though, she checks `mt_is_approved` and sees that she did successfully approve Bazaar, despite the error. +4. [Approval IDs](#4-approval-ids): Bob buys Alice's token via Market. +5. [Approval IDs, edge case](#5-approval-ids-edge-case): Bob transfers same token back to Alice, Alice re-approves Market & Bazaar. Bazaar has an outdated cache. Bob tries to buy from Bazaar at the old price. +6. [Revoke one](#6-revoke-one): Alice revokes Market's approval for this token. +7. [Revoke all](#7-revoke-all): Alice revokes all approval for this token. + +### 1. Simple Approval + +Alice approves Bob to transfer her tokens. + +**High-level explanation** + +1. Alice approves Bob +2. Alice queries the token to verify + +**Technical calls** + +1. Alice calls `mt::mt_approve({ "token_ids": ["1","2"], amounts:["1","100"], "account_id": "bob" })`. She attaches 1 yoctoⓃ, (.000000000000000000000001Ⓝ). Using [NEAR CLI](https://docs.near.org/docs/tools/near-cli) to make this call, the command would be: + + near call mt mt_approve \ + '{ "token_ids": ["1","2"], amounts: ["1","100"], "account_id": "bob" }' \ + --accountId alice --amount .000000000000000000000001 + + The response: + + '' +2. Alice calls view method `mt_is_approved`: + + near view mt mt_is_approved \ + '{ "token_ids": ["1", "2"], amounts:["1","100"], "approved_account_id": "bob" }' + + The response: + + true + +### 3. Approval with cross-contract call + +Alice approves Market to transfer some of her tokens and passes `msg` so that MT will call `mt_on_approve` on Market's contract. She probably does this via Market's frontend app which would know how to construct `msg` in a useful way. + +**High-level explanation** + +1. Alice calls `mt_approve` to approve `market` to transfer her token, and passes a `msg` +2. Since `msg` is included, `mt` will schedule a cross-contract call to `market` +3. Market can do whatever it wants with this info, such as listing the token for sale at a given price. The result of this operation is returned as the promise outcome to the original `mt_approve` call. + +**Technical calls** + +1. Using near-cli: + + near call mt mt_approve '{ + "token_ids": ["1","2"], + "amounts": ["1", "100"], + "account_id": "market", + "msg": "{\"action\": \"list\", \"price\": [\"100\",\"50\"],\"token\": \"nDAI\" }" + }' --accountId alice --amount .000000000000000000000001 + + At this point, near-cli will hang until the cross-contract call chain fully resolves, which would also be true if Alice used a Market frontend using [near-api-js](https://docs.near.org/docs/develop/front-end/near-api-js). Alice's part is done, though. The rest happens behind the scenes. + +2. `mt` schedules a call to `mt_on_approve` on `market`. Using near-cli notation for easy cross-reference with the above, this would look like: + + near call market mt_on_approve '{ + "token_ids": ["1","2"], + "amounts": ["1","100"], + "owner_id": "alice", + "approval_ids": ["4","5"], + "msg": "{\"action\": \"list\", \"price\": [\"100\",\"50\"], \"token\": \"nDAI\" }" + }' --accountId mt + +3. `market` now knows that it can sell Alice's tokens for 100 [nDAI](https://explorer.mainnet.near.org/accounts/6b175474e89094c44da98b954eedeac495271d0f.factory.bridge.near) and 50 [nDAI](https://explorer.mainnet.near.org/accounts/6b175474e89094c44da98b954eedeac495271d0f.factory.bridge.near), and that when it transfers it to a buyer using `mt_batch_transfer`, it can pass along the given `approval_ids` to ensure that Alice hasn't changed her mind. It can schedule any further cross-contract calls it wants, and if it returns these promises correctly, Alice's initial near-cli call will resolve with the outcome from the final step in the chain. If Alice actually made this call from a Market frontend, the frontend can use this return value for something useful. + +### 3. Approval with cross-contract call, edge case + +Alice approves Bazaar and passes `msg` again. Maybe she actually does this via near-cli, rather than using Bazaar's frontend, because what's this? Bazaar doesn't implement `mt_on_approve`, so Alice sees an error in the transaction result. + +Not to worry, though, she checks `mt_is_approved` and sees that she did successfully approve Bazaar, despite the error. She will have to find a new way to list her token for sale in Bazaar, rather than using the same `msg` shortcut that worked for Market. + +**High-level explanation** + +1. Alice calls `mt_approve` to approve `bazaar` to transfer her token, and passes a `msg`. +2. Since `msg` is included, `mt` will schedule a cross-contract call to `bazaar`. +3. Bazaar doesn't implement `mt_on_approve`, so this call results in an error. The approval still worked, but Alice sees an error in her near-cli output. +4. Alice checks if `bazaar` is approved, and sees that it is, despite the error. + +**Technical calls** + +1. Using near-cli: + + near call mt mt_approve '{ + "token_ids": ["1"], + "amounts: ["1000"], + "account_id": "bazaar", + "msg": "{\"action\": \"list\", \"price\": \"100\", \"token\": \"nDAI\" }" + }' --accountId alice --amount .000000000000000000000001 + +2. `mt` schedules a call to `mt_on_approve` on `market`. Using near-cli notation for easy cross-reference with the above, this would look like: + + near call bazaar mt_on_approve '{ + "token_ids": ["1"], + "amounts": ["1000"], + "owner_id": "alice", + "approval_ids": [3], + "msg": "{\"action\": \"list\", \"price\": \"100\", \"token\": \"nDAI\" }" + }' --accountId mt + +3. 💥 `bazaar` doesn't implement this method, so the call results in an error. Alice sees this error in the output from near-cli. + +4. Alice checks if the approval itself worked, despite the error on the cross-contract call: + + near view mt mt_is_approved \ + '{ "token_ids": ["1","2"], "amounts":["1","100"], "approved_account_id": "bazaar" }' + + The response: + + true + +### 4. Approval IDs + +Bob buys Alice's token via Market. Bob probably does this via Market's frontend, which will probably initiate the transfer via a call to `ft_transfer_call` on the nDAI contract to transfer 100 nDAI to `market`. Like the MT standard's "transfer and call" function, [Fungible Token](../FungibleToken/Core.md)'s `ft_transfer_call` takes a `msg` which `market` can use to pass along information it will need to pay Alice and actually transfer the MT. The actual transfer of the MT is the only part we care about here. + +**High-level explanation** + +1. Bob signs some transaction which results in the `market` contract calling `mt_transfer` on the `mt` contract, as described above. To be trustworthy and pass security audits, `market` needs to pass along `approval_id` so that it knows it has up-to-date information. + +**Technical calls** + +Using near-cli notation for consistency: + + near call mt mt_transfer '{ + "receiver_id": "bob", + "token_id": "1", + "amount": "1", + "approval_id": 2, + }' --accountId market --amount .000000000000000000000001 + +### 5. Approval IDs, edge case +Bob transfers same token back to Alice, Alice re-approves Market & Bazaar, listing her token at a higher price than before. Bazaar is somehow unaware of these changes, and still stores `approval_id: 3` internally along with Alice's old price. Bob tries to buy from Bazaar at the old price. Like the previous example, this probably starts with a call to a different contract, which eventually results in a call to `mt_transfer` on `bazaar`. Let's consider a possible scenario from that point. + +**High-level explanation** + +Bob signs some transaction which results in the `bazaar` contract calling `mt_transfer` on the `mt` contract, as described above. To be trustworthy and pass security audits, `bazaar` needs to pass along `approval_id` so that it knows it has up-to-date information. It does not have up-to-date information, so the call fails. If the initial `mt_transfer` call is part of a call chain originating from a call to `ft_transfer_call` on a fungible token, Bob's payment will be refunded and no assets will change hands. + +**Technical calls** + +Using near-cli notation for consistency: + + near call mt mt_transfer '{ + "receiver_id": "bob", + "token_id": "1", + "amount": "1", + "approval_id": 3, + }' --accountId bazaar --amount .000000000000000000000001 + +### 6. Revoke one + +Alice revokes Market's approval for this token. + +**Technical calls** + +Using near-cli: + + near call mt mt_revoke '{ + "account_id": "market", + "token_ids": ["1"], + }' --accountId alice --amount .000000000000000000000001 + +Note that `market` will not get a cross-contract call in this case. The implementors of the Market app should implement [cron](https://en.wikipedia.org/wiki/Cron)-type functionality to intermittently check that Market still has the access they expect. + +### 7. Revoke all + +Alice revokes all approval for these tokens + +**Technical calls** + +Using near-cli: + + near call mt mt_revoke_all '{ + "token_ids": ["1", "2"], + }' --accountId alice --amount .000000000000000000000001 + +Again, note that no previous approvers will get cross-contract calls in this case. + + +## Reference-level explanation + +The `TokenApproval` structure returned by `mt_token_approvals` returns `approved_account_ids` field, which is a map of account IDs to `Approval` and `approval_owner_id` which is the associated account approved for removal from. The `amount` field though wrapped in quotes and treated like strings, the number will be stored as an unsigned integer with 128 bits. + in approval is Using TypeScript's [Record type](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeystype) notation: + +```diff ++ type Approval = { ++ amount: string ++ approval_id: string ++ } ++ ++ type TokenApproval = { ++ approval_owner_id: string, ++ approved_account_ids: Record, ++ }; +``` + +Example token approval data: + +```json +[{ + "approval_owner_id": "alice.near", + "approved_account_ids": { + "bob.near": { + "amount": "100", + "approval_id":1, + }, + "carol.near": { + "amount":"2", + "approval_id": 2, + } + } +}] +``` + +### What is an "approval ID"? + +This is a unique number given to each approval that allows well-intentioned marketplaces or other 3rd-party MT resellers to avoid a race condition. The race condition occurs when: + +1. A token is listed in two marketplaces, which are both saved to the token as approved accounts. +2. One marketplace sells the token, which clears the approved accounts. +3. The new owner sells back to the original owner. +4. The original owner approves the token for the second marketplace again to list at a new price. But for some reason the second marketplace still lists the token at the previous price and is unaware of the transfers happening. +5. The second marketplace, operating from old information, attempts to again sell the token at the old price. + +Note that while this describes an honest mistake, the possibility of such a bug can also be taken advantage of by malicious parties via [front-running](https://defi.cx/front-running-ethereum/). + +To avoid this possibility, the MT contract generates a unique approval ID each time it approves an account. Then when calling `mt_transfer`, `mt_transfer_call`, `mt_batch_transfer`, or `mt_batch_transfer_call` the approved account passes `approval_id` or `approval_ids` with this value to make sure the underlying state of the token(s) hasn't changed from what the approved account expects. + +Keeping with the example above, say the initial approval of the second marketplace generated the following `approved_account_ids` data: + +```json +{ + "approval_owner_id": "alice.near", + "approved_account_ids": { + "marketplace_1.near": { + "approval_id": 1, + "amount": "100", + }, + "marketplace_2.near": 2, + "approval_id": 2, + "amount": "50", + } +} +``` + +But after the transfers and re-approval described above, the token might have `approved_account_ids` as: + +```json +{ + "approval_owner_id": "alice.near", + "approved_account_ids": { + "marketplace_2.near": { + "approval_id": 3, + "amount": "50", + } + } +} +``` + +The marketplace then tries to call `mt_transfer`, passing outdated information: + +```bash +# oops! +near call mt-contract.near mt_transfer '{"account_id": "someacct", "amount":"50", "approval_id": 2 }' +``` + + +### Interface + +The MT contract must implement the following methods: + +```ts +/******************/ +/* CHANGE METHODS */ +/******************/ + +// Add an approved account for a specific set of tokens. +// +// Requirements +// * Caller of the method must attach a deposit of at least 1 yoctoⓃ for +// security purposes +// * Contract MAY require caller to attach larger deposit, to cover cost of +// storing approver data +// * Contract MUST panic if called by someone other than token owner +// * Contract MUST panic if addition would cause `mt_revoke_all` to exceed +// single-block gas limit. See below for more info. +// * Contract MUST increment approval ID even if re-approving an account +// * If successfully approved or if had already been approved, and if `msg` is +// present, contract MUST call `mt_on_approve` on `account_id`. See +// `mt_on_approve` description below for details. +// +// Arguments: +// * `token_ids`: the token ids for which to add an approval +// * `account_id`: the account to add to `approved_account_ids` +// * `amounts`: the number of tokens to approve for transfer, wrapped in quotes and treated +// like an array of string, although the numbers will be stored as an array of +// unsigned integer with 128 bits. + +// * `msg`: optional string to be passed to `mt_on_approve` +// +// Returns void, if no `msg` given. Otherwise, returns promise call to +// `mt_on_approve`, which can resolve with whatever it wants. +function mt_approve( + token_ids: [string], + amounts: [string], + account_id: string, + msg: string|null, +): void|Promise {} + +// Revoke an approved account for a specific token. +// +// Requirements +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * If contract requires >1yN deposit on `mt_approve`, contract +// MUST refund associated storage deposit when owner revokes approval +// * Contract MUST panic if called by someone other than token owner +// +// Arguments: +// * `token_ids`: the token for which to revoke approved_account_ids +// * `account_id`: the account to remove from `approvals` +function mt_revoke( + token_ids: [string], + account_id: string +) {} + +// Revoke all approved accounts for a specific token. +// +// Requirements +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * If contract requires >1yN deposit on `mt_approve`, contract +// MUST refund all associated storage deposit when owner revokes approved_account_ids +// * Contract MUST panic if called by someone other than token owner +// +// Arguments: +// * `token_ids`: the token ids with approved_account_ids to revoke +function mt_revoke_all(token_ids: [string]) {} + +/****************/ +/* VIEW METHODS */ +/****************/ + +// Check if tokens are approved for transfer by a given account, optionally +// checking an approval_id +// +// Requirements: +// * Contract MUST panic if `approval_ids` is not null and the length of +// `approval_ids` is not equal to `token_ids` +// +// Arguments: +// * `token_ids`: the tokens for which to check an approval +// * `approved_account_id`: the account to check the existence of in `approved_account_ids` +// * `amounts`: specify the positionally corresponding amount for the `token_id` +// that at least must be approved. The number of tokens to approve for transfer, +// wrapped in quotes and treated like an array of string, although the numbers will be +// stored as an array of unsigned integer with 128 bits. +// * `approval_ids`: an optional array of approval IDs to check against +// current approval IDs for given account and `token_ids`. +// +// Returns: +// if `approval_ids` is given, `true` if `approved_account_id` is approved with given `approval_id` +// and has at least the amount specified approved otherwise, `true` if `approved_account_id` +// is in list of approved accounts and has at least the amount specified approved +// finally it returns false for all other states +function mt_is_approved( + token_ids: [string], + approved_account_id: string, + amounts: [string], + approval_ids: number[]|null +): boolean {} + +// Get a the list of approvals for a given token_id and account_id +// +// Arguments: +// * `token_id`: the token for which to check an approval +// * `account_id`: the account to retrieve approvals for +// +// Returns a TokenApproval object, as described in Approval Management standard +function mt_token_approval( + token_id: string, + account_id: string, +): TokenApproval {} + + +// Get a list of all approvals for a given token_id +// +// Arguments: +// * `from_index`: a string representing an unsigned 128-bit integer, +// representing the starting index of tokens to return +// * `limit`: the maximum number of tokens to return +// +// Returns an array of TokenApproval objects, as described in Approval Management standard, and an empty array if there are no approvals +function mt_token_approvals( + token_id: string, + from_index: string|null, // default: "0" + limit: number|null, +): TokenApproval[] {} +``` + +### Why must `mt_approve` panic if `mt_revoke_all` would fail later? + +In the description of `mt_approve` above, it states: + + Contract MUST panic if addition would cause `mt_revoke_all` to exceed + single-block gas limit. + +What does this mean? + +First, it's useful to understand what we mean by "single-block gas limit". This refers to the [hard cap on gas per block at the protocol layer](https://docs.near.org/docs/concepts/gas#thinking-in-gas). This number will increase over time. + +Removing data from a contract uses gas, so if an MT had a large enough number of approvals, `mt_revoke_all` would fail, because calling it would exceed the maximum gas. + +Contracts must prevent this by capping the number of approvals for a given token. However, it is up to contract authors to determine a sensible cap for their contract (and the single block gas limit at the time they deploy). Since contract implementations can vary, some implementations will be able to support a larger number of approvals than others, even with the same maximum gas per block. + +Contract authors may choose to set a cap of something small and safe like 10 approvals, or they could dynamically calculate whether a new approval would break future calls to `mt_revoke_all`. But every contract MUST ensure that they never break the functionality of `mt_revoke_all`. + + +### Approved Account Contract Interface + +If a contract that gets approved to transfer MTs wants to, it can implement `mt_on_approve` to update its own state when granted approval for a token: + +```ts +// Respond to notification that contract has been granted approval for a token. +// +// Notes +// * Contract knows the token contract ID from `predecessor_account_id` +// +// Arguments: +// * `token_ids`: the token_ids to which this contract has been granted approval +// * `amounts`: the ositionally corresponding amount for the token_id +// that at must be approved. The number of tokens to approve for transfer, +// wrapped in quotes and treated like an array of string, although the numbers will be +// stored as an array of unsigned integer with 128 bits. +// * `owner_id`: the owner of the token +// * `approval_ids`: the approval ID stored by NFT contract for this approval. +// Expected to be a number within the 2^53 limit representable by JSON. +// * `msg`: specifies information needed by the approved contract in order to +// handle the approval. Can indicate both a function to call and the +// parameters to pass to that function. +function mt_on_approve( + token_ids: [TokenId], + amounts: [string], + owner_id: string, + approval_ids: [number], + msg: string, +) {} +``` + +Note that the MT contract will fire-and-forget this call, ignoring any return values or errors generated. This means that even if the approved account does not have a contract or does not implement `mt_on_approve`, the approval will still work correctly from the point of view of the MT contract. + +Further note that there is no parallel `mt_on_revoke` when revoking either a single approval or when revoking all. This is partially because scheduling many `mt_on_revoke` calls when revoking all approvals could incur prohibitive [gas fees](https://docs.near.org/docs/concepts/gas). Apps and contracts which cache MT approvals can therefore not rely on having up-to-date information, and should periodically refresh their caches. Since this will be the necessary reality for dealing with `mt_revoke_all`, there is no reason to complicate `mt_revoke` with an `mt_on_revoke` call. + +### No incurred cost for core MT behavior + +MT contracts should be implemented in a way to avoid extra gas fees for serialization & deserialization of `approved_account_ids` for calls to `mt_*` methods other than `mt_tokens`. See `near-contract-standards` [implementation of `ft_metadata` using `LazyOption`](https://github.com/near/near-sdk-rs/blob/c2771af7fdfe01a4e8414046752ee16fb0d29d39/examples/fungible-token/ft/src/lib.rs#L71) as a reference example. \ No newline at end of file diff --git a/specs/Standards/MultiToken/Core.md b/specs/Standards/MultiToken/Core.md new file mode 100644 index 000000000..8a1770d83 --- /dev/null +++ b/specs/Standards/MultiToken/Core.md @@ -0,0 +1,399 @@ +# Multi Token ([NEP-245](https://github.com/near/NEPs/discussions/246)) + +:::caution +This is part of the proposed spec [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) and is subject to change. +::: + +Version `1.0.0` + +## Summary + +A standard interface for a multi token standard that supports fungible, semi-fungible,non-fungible, and tokens of any type, allowing for ownership, transfer, and batch transfer of tokens regardless of specific type. + +## Motivation + + +In the three years since [ERC-1155] was ratified by the Ethereum Community, Multi Token based contracts have proven themselves valuable assets. Many blockchain projects emulate this standard for representing multiple token assets classes in a single contract. The ability to reduce transaction overhead for marketplaces, video games, DAOs, and exchanges is appealing to the blockchain ecosystem and simplifies transactions for developers. + +Having a single contract represent NFTs, FTs, and tokens that sit in-between greatly improves efficiency. The standard also introduced the ability to make batch requests with multiple asset classes reducing complexity. This standard allows operations that currently require _many_ transactions to be completed in a single transaction that can transfer not only NFTs and FTs, but any tokens that are a part of same token contract. + +With this standard, we have sought to take advantage of the ability of the NEAR blockchain to scale. Its sharded runtime, and [storage staking] model that decouples [gas] fees from storage demand, enables ultra low transaction fees and greater on chain storage ( see [Metadata] extension). + +With the aforementioned, it is noteworthy to mention that like the [NFT] standard the Multi Token standard, implements `mt_transfer_call`, +which allows, a user to attach many tokens to a call to a separate contract. Additionally, this standard includes an optional [Approval Management] extension. The extension allows marketplaces to trade on behalf of a user, providing additional flexibility for dApps. + +Prior art: + +- [ERC-721] +- [ERC-1155] +- [NEAR Fungible Token Standard][FT], which first pioneered the "transfer and call" technique +- [NEAR Non-Fungible Token Standard][NFT] +## Rationale + +Why have another standard, aren't fungible and non-fungible tokens enough? The current fungible token and non-fungible token standards, do not provide support for representing many FT tokens in a single contract, as well as the flexibility to define different token types with different behavior in a single contract. This is something that makes it difficult to be interoperable with other major blockchain networks, that implement standards that allow for representation of many different FT tokens in a single contract such as Ethereum. + +The standard here introduces a few concepts that evolve the original [ERC-1155] standard to have more utility, while maintaining the original flexibility of the standard. So keeping that in mind, we are defining this as a new token type. It combines two main features of FT and NFT. It allows us to represent many token types in a single contract, and it's possible to store the amount for each token. + +The decision to not use FT and NFT as explicit token types was taken to allow the community to define their own standards and meanings through metadata. As standards evolve on other networks, this specification allows the standard to be able to represent tokens across networks accurately, without necessarily restricting the behavior to any preset definition. + +The issues with this in general is a problem with defining what metadata means and how is that interpreted. We have chosen to follow the pattern that is currently in use on Ethereum in the [ERC-1155] standard. That pattern relies on people to make extensions or to make signals as to how they want the metadata to be represented for their use case. + +One of the areas that has broad sweeping implications from the [ERC-1155] standard is the lack of direct access to metadata. With Near's sharding we are able to have a [Metadata Extension](Metadata.md) for the standard that exists on chain. So developers and users are not required to use an indexer to understand, how to interact or interpret tokens, via token identifiers that they receive. + +Another extension that we made was to provide an explicit ability for developers and users to group or link together series of NFTs/FTs or any combination of tokens. This provides additional flexiblity that the [ERC-1155] standard only has loose guidelines on. This was chosen to make it easy for consumers to understand the relationship between tokens within the contract. + +To recap, we choose to create this standard, to improve interoperability, developer ease of use, and to extend token representability beyond what was available directly in the FT or NFT standards. We believe this to be another tool in the developer's toolkit. It makes it possible to represent many types of tokens and to enable exchanges of many tokens within a single `transaction`. + +## Reference-level explanation + +**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 multi token standard but instead in the [Storage Standard](../StorageManagement.md). +- To prevent the deployed contract from being modified or deleted, it should not have any access keys on its account. + +### MT Interface + +```ts +// The base structure that will be returned for a token. If contract is using +// extensions such as Approval Management, Enumeration, Metadata, or other +// attributes may be included in this structure. +type Token = { + token_id: string, + owner_id: string | null +} + + +/******************/ +/* 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 +// * Caller must have greater than or equal to the `amount` being requested +// * 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 +// * `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// * `approval` (optional): is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_transfer( + receiver_id: string, + token_id: string, + amount: string, + approval: [owner_id: string, approval_id: number]|null, + memo: string|null, +) {} + +// Simple batch transfer. Transfer a given `token_ids` from current owner to +// `receiver_id`. +// +// Requirements +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security purposes +// * Caller must have greater than or equal to the `amounts` being requested for the given `token_ids` +// * 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. +// * Contract MUST panic if called with the length of `token_ids` not equal to `amounts` is not equal +// * Contract MUST panic if `approval_ids` is not `null` and does not equal the length of `token_ids` +// +// Arguments: +// * `receiver_id`: the valid NEAR account receiving the token +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like an array of strings, although the numbers will be stored as an array of unsigned integer +// with 128 bits. +// * `approvals` (optional): is an array of expected `approval` per `token_ids`. +// If a `token_id` does not have a corresponding `approval` then the entry in the array +// must be marked null. +// `approval` is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_batch_transfer( + receiver_id: string, + token_ids: string[], + amounts: string[], + approvals: ([owner_id: string, approval_id: number]| null)[]| null, + memo: string|null, +) {} + + +// 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 MT +// contract at the method `mt_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 Multi Token, token in a call to a +// receiver contract. +// +// Requirements: +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * Caller must have greater than or equal to the `amount` being requested +// * 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 `mt_on_transfer` according to the +// standard. If it does not, MT contract's `mt_resolve_transfer` MUST deal +// with the resulting failed cross-contract call and roll back the transfer. +// * Contract MUST implement the behavior described in `mt_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. +// * `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// * `owner_id`: the valid NEAR account that owns the token +// * `approval` (optional): is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the expected approval ID. A number smaller than +// 2^53, and therefore representable as JSON. See Approval Management +// * `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 mt_transfer_call( + receiver_id: string, + token_id: string, + amount: string, + approval: [owner_id: string, approval_id: number]|null, + memo: string|null, + msg: string, +): Promise {} + + + +// Transfer tokens and call a method on a receiver contract. A successful +// workflow will end in a success execution outcome to the callback on the MT +// contract at the method `mt_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 Multi Token, token in a call to a +// receiver contract. +// +// Requirements: +// * Caller of the method must attach a deposit of 1 yoctoⓃ for security +// purposes +// * Caller must have greater than or equal to the `amount` being requested +// * 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 `mt_on_transfer` according to the +// standard. If it does not, MT contract's `mt_resolve_transfer` MUST deal +// with the resulting failed cross-contract call and roll back the transfer. +// * Contract MUST implement the behavior described in `mt_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. +// * Contract MUST panic if called with the length of `token_ids` not equal to `amounts` is not equal +// * Contract MUST panic if `approval_ids` is not `null` and does not equal the length of `token_ids` +// +// Arguments: +// * `receiver_id`: the valid NEAR account receiving the token. +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like an array of string, although the numbers will be stored as an array of +// unsigned integer with 128 bits. +// * `approvals` (optional): is an array of expected `approval` per `token_ids`. +// If a `token_id` does not have a corresponding `approval` then the entry in the array +// must be marked null. +// `approval` is a tuple of [`owner_id`,`approval_id`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the 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 mt_batch_transfer_call( + receiver_id: string, + token_ids: string[], + amounts: string[], + approvals: ([owner_id: string, approval_id: number]|null)[] | null, + memo: string|null, + msg: string, +): Promise {} + +/****************/ +/* VIEW METHODS */ +/****************/ + + +// Returns the tokens with the given `token_ids` or `null` if no such token. +function mt_token(token_ids: string[]) (Token | null)[] + +// Returns the balance of an account for the given `token_id`. +// The balance though wrapped in quotes and treated like a string, +// the number will be stored as an unsigned integer with 128 bits. +// Arguments: +// * `account_id`: the NEAR account that owns the token. +// * `token_id`: the token to retrieve the balance from +function mt_balance_of(account_id: string, token_id: string): string + +// Returns the balances of an account for the given `token_ids`. +// The balances though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +// Arguments: +// * `account_id`: the NEAR account that owns the tokens. +// * `token_ids`: the tokens to retrieve the balance from +function mt_batch_balance_of(account_id: string, token_ids: string[]): string[] + +// Returns the token supply with the given `token_id` or `null` if no such token exists. +// The supply though wrapped in quotes and treated like a string, the number will be stored +// as an unsigned integer with 128 bits. +function mt_supply(token_id: string): string | null + +// Returns the token supplies with the given `token_ids`, a string value is returned or `null` +// if no such token exists. The supplies though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +function mt_batch_supply(token_ids: string[]): (string | null)[] +``` + +The following behavior is required, but contract authors may name this function something other than the conventional `mt_resolve_transfer` used here. + +```ts +// Finalize an `mt_transfer_call` or `mt_batch_transfer_call` chain of cross-contract calls. Generically +// referred to as `mt_transfer_call` as it applies to `mt_batch_transfer_call` as well. +// +// The `mt_transfer_call` process: +// +// 1. Sender calls `mt_transfer_call` on MT contract +// 2. MT contract transfers token from sender to receiver +// 3. MT contract calls `mt_on_transfer` on receiver contract +// 4+. [receiver contract may make other cross-contract calls] +// N. MT contract resolves promise chain with `mt_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 +// `sender_id` +// +// Arguments: +// * `sender_id`: the sender of `mt_transfer_call` +// * `receiver_id`: the `receiver_id` argument given to `mt_transfer_call` +// * `token_ids`: the `token_ids` argument given to `mt_transfer_call` +// * `amounts`: the `token_ids` argument given to `mt_transfer_call` +// * `approvals (optional)`: if using Approval Management, contract MUST provide +// set of original approvals in this argument, and restore the +// approved accounts in case of revert. +// `approvals` is an array of expected `approval_list` per `token_ids`. +// If a `token_id` does not have a corresponding `approvals_list` then the entry in the +// array must be marked null. +// `approvals_list` is an array of triplets of [`owner_id`,`approval_id`,`amount`]. +// `owner_id` is the valid Near account that owns the tokens. +// `approval_id` is the expected approval ID. A number smaller than +// 2^53, and therefore representable as JSON. See Approval Management +// standard for full explanation. +// `amount`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the number will be stored as an unsigned integer +// with 128 bits. +// +// +// +// Returns total amount spent by the `receiver_id`, corresponding to the `token_id`. +// The amounts returned, though wrapped in quotes and treated like strings, +// the numbers will be stored as an unsigned integer with 128 bits. +// Example: if sender_id calls `mt_transfer_call({ "amounts": ["100"], token_ids: ["55"], receiver_id: "games" })`, +// but `receiver_id` only uses 80, `mt_on_transfer` will resolve with `["20"]`, and `mt_resolve_transfer` +// will return `["80"]`. + + +function mt_resolve_transfer( + sender_id: string, + receiver_id: string, + token_ids: string[], + approvals: (null | [owner_id: string, approval_id: number, amount: string][]) []| null +):string[] {} +``` + +### Receiver Interface + +Contracts which want to make use of `mt_transfer_call` and `mt_batch_transfer_call` must implement the following: + +```ts +// Take some action after receiving a multi token +// +// Requirements: +// * Contract MUST restrict calls to this function to a set of whitelisted +// contracts +// * Contract MUST panic if `token_ids` length does not equals `amounts` +// length +// * Contract MUST panic if `previous_owner_ids` length does not equals `token_ids` +// length +// +// Arguments: +// * `sender_id`: the sender of `mt_transfer_call` +// * `previous_owner_ids`: the account that owned the tokens prior to it being +// transferred to this contract, which can differ from `sender_id` if using +// Approval Management extension +// * `token_ids`: the `token_ids` argument given to `mt_transfer_call` +// * `amounts`: the `token_ids` argument given to `mt_transfer_call` +// * `msg`: information necessary for this contract to know how to process the +// request. This may include method names and/or arguments. +// +// Returns the number of unused tokens in string form. For instance, if `amounts` +// is `["10"]` but only 9 are needed, it will return `["1"]`. The amounts returned, +// though wrapped in quotes and treated like strings, the numbers will be stored as +// an unsigned integer with 128 bits. + + +function mt_on_transfer( + sender_id: string, + previous_owner_ids: string[], + token_ids: string[], + amounts: string[], + msg: string, +): Promise; +``` + + [ERC-721]: https://eips.ethereum.org/EIPS/eip-721 + [ERC-1155]: https://eips.ethereum.org/EIPS/eip-1155 + [storage staking]: https://docs.near.org/docs/concepts/storage-staking + [gas]: https://docs.near.org/docs/concepts/gas + [Metadata]: Metadata.md + [NFT]: ../NonFungibleToken/Core.md + [Approval Management]: ApprovalManagement.md + [FT]: ../FungibleToken/Core.md + diff --git a/specs/Standards/MultiToken/Enumeration.md b/specs/Standards/MultiToken/Enumeration.md new file mode 100644 index 000000000..7a0b560ba --- /dev/null +++ b/specs/Standards/MultiToken/Enumeration.md @@ -0,0 +1,84 @@ +# Multi Token Enumeration([NEP-245](https://github.com/near/NEPs/discussions/246)) + +:::caution +This is part of the proposed spec [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) and is subject to change. +::: + +Version `1.0.0` + +## Summary + +Standard interfaces for counting & fetching tokens, for an entire Multi Token contract or for a given owner. + +## Motivation + +Apps such as marketplaces and wallets need a way to show all tokens owned by a given account and to show statistics about all tokens for a given contract. This extension provides a standard way to do so. + +While some Multi Token contracts may forego this extension to save [storage] costs, this requires apps to have custom off-chain indexing layers. This makes it harder for apps to integrate with such Multi Token contracts. Apps which integrate only with Multi Token Standards that use the Enumeration extension do not even need a server-side component at all, since they can retrieve all information they need directly from the blockchain. + +Prior art: + +- [ERC-721]'s enumeration extension +- [Non Fungible Token Standard's](../NonFungibleToken/Enumeration.md) enumeration extension + +## Interface + +The contract must implement the following view methods: + +// Metadata field is optional if metadata extension is implemented. Includes the base token metadata id and the token_metadata object, that represents the token specific metadata. + +```ts +// Get a list of all tokens +// +// Arguments: +// * `from_index`: a string representing an unsigned 128-bit integer, +// representing the starting index of tokens to return +// * `limit`: the maximum number of tokens to return +// +// Returns an array of `Token` objects, as described in the Core standard, +// and an empty array if there are no tokens +function mt_tokens( + from_index: string|null, // default: "0" + limit: number|null, // default: unlimited (could fail due to gas limit) +): Token[] {} + +// Get list of all tokens owned by a given account +// +// Arguments: +// * `account_id`: a valid NEAR account +// * `from_index`: a string representing an unsigned 128-bit integer, +// representing the starting index of tokens to return +// * `limit`: the maximum number of tokens to return +// +// Returns a paginated list of all tokens owned by this account, and an empty array if there are no tokens +function mt_tokens_for_owner( + account_id: string, + from_index: string|null, // default: 0 + limit: number|null, // default: unlimited (could fail due to gas limit) +): Token[] {} +``` + +The contract must implement the following view methods if using metadata extension: + +```ts +// Get list of all base metadata for the contract +// +// Arguments: +// * `from_index`: a string representing an unsigned 128-bit integer, +// representing the starting index of tokens to return +// * `limit`: the maximum number of tokens to return +// +// Returns an array of `MTBaseTokenMetadata` objects, as described in the Metadata standard, and an empty array if there are no tokens +function mt_tokens_base_metadata_all( + from_index: string | null, + limit: number | null + ): MTBaseTokenMetadata[] +``` + + +## Notes + +At the time of this writing, the specialized collections in the `near-sdk` Rust crate are iterable, but not all of them have implemented an `iter_from` solution. There may be efficiency gains for large collections and contract developers are encouraged to test their data structures with a large amount of entries. + + [ERC-721]: https://eips.ethereum.org/EIPS/eip-721 + [storage]: https://docs.near.org/docs/concepts/storage-staking diff --git a/specs/Standards/MultiToken/Events.md b/specs/Standards/MultiToken/Events.md new file mode 100644 index 000000000..3076b5b85 --- /dev/null +++ b/specs/Standards/MultiToken/Events.md @@ -0,0 +1,187 @@ +# Multi Token Event([NEP-245](https://github.com/near/NEPs/discussions/246)) + +:::caution +This is part of the proposed spec [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) and is subject to change. +::: + +Version `1.0.0` + +## Summary + +Standard interfaces for Multi Token Contract actions. +Extension of [NEP-297](../EventsFormat.md) + +## Motivation + +NEAR and third-party applications need to track + `mint`, `burn`, `transfer` events for all MT-driven apps consistently. This exension addresses that. + +Note that applications, including NEAR Wallet, could require implementing additional methods to display tokens correctly such as [`mt_metadata`](Metadata.md) and [`mt_tokens_for_owner`](Enumeration.md). + +## Interface +Multi Token Events MUST have `standard` set to `"nep245"`, standard version set to `"1.0.0"`, `event` value is one of `mt_mint`, `mt_burn`, `mt_transfer`, and `data` must be of one of the following relavant types: `MtMintLog[] | MtBurnLog[] | MtTransferLog[]`: + + + +```ts +interface MtEventLogData { + EVENT_JSON: { + standard: "nep245", + version: "1.0.0", + event: MtEvent, + data: MtMintLog[] | MtBurnLog[] | MtTransferLog[] + } +} +``` + +```ts +// Minting event log. Emitted when a token is minted/created. +// Requirements +// * Contract MUST emit event when minting a token +// Fields +// * Contract token_ids and amounts MUST be the same length +// * `owner_id`: the account receiving the minted token +// * `token_ids`: the tokens minted +// * `amounts`: the number of tokens minted, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +// * `memo`: optional message +interface MtMintLog { + owner_id: string, + token_ids: string[], + amounts: string[], + memo?: string +} + +// Burning event log. Emitted when a token is burned. +// Requirements +// * Contract MUST emit event when minting a token +// Fields +// * Contract token_ids and amounts MUST be the same length +// * `owner_id`: the account whose token(s) are being burned +// * `authorized_id`: approved account_id to burn, if applicable +// * `token_ids`: the tokens being burned +// * `amounts`: the number of tokens burned, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +// * `memo`: optional message +interface MtBurnLog { + owner_id: string, + authorized_id?: string, + token_ids: string[], + amounts: string[], + memo?: string +} + +// Transfer event log. Emitted when a token is transferred. +// Requirements +// * Contract MUST emit event when transferring a token +// Fields +// * `authorized_id`: approved account_id to transfer +// * `old_owner_id`: the account sending the tokens "sender.near" +// * `new_owner_id`: the account receiving the tokens "receiver.near" +// * `token_ids`: the tokens to transfer +// * `amounts`: the number of tokens to transfer, wrapped in quotes and treated +// like a string, although the numbers will be stored as an unsigned integer +// array with 128 bits. +interface MtTransferLog { + authorized_id?: string, + old_owner_id: string, + new_owner_id: string, + token_ids: string[], + amounts: string[], + memo?: string +} +``` + +## Examples + +Single owner minting (pretty-formatted for readability purposes): + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_mint", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts":["1", "100"]} + ] +} +``` + +Different owners minting: + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_mint", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts":["1","100"]}, + {"owner_id": "user1.near", "token_ids": ["meme"], "amounts": ["1"]} + ] +} +``` + +Different events (separate log entries): + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_burn", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora", "proximitylabs_ft"], "amounts": ["1","100"]}, + ] +} +``` + +Authorized id: + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_burn", + "data": [ + {"owner_id": "foundation.near", "token_ids": ["aurora_alpha", "proximitylabs_ft"], "amounts": ["1","100"], "authorized_id": "thirdparty.near" }, + ] +} +``` + +```js +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_transfer", + "data": [ + {"old_owner_id": "user1.near", "new_owner_id": "user2.near", "token_ids": ["meme"], "amounts":["1"], "memo": "have fun!"} + ] +} + +EVENT_JSON:{ + "standard": "nep245", + "version": "1.0.0", + "event": "mt_transfer", + "data": [ + {"old_owner_id": "user2.near", "new_owner_id": "user3.near", "token_ids": ["meme"], "amounts":["1"], "authorized_id": "thirdparty.near", "memo": "have fun!"} + ] +} +``` + +## Further methods + +Note that the example events covered above cover two different kinds of events: +1. Events that are not specified in the MT Standard (`mt_mint`, `mt_burn`) +2. An event that is covered in the [Multi Token Core Standard](https://nomicon.io/Standards/MultiToken/Core.html#mt-interface). (`mt_transfer`) + +This event standard also applies beyond the three events highlighted here, where future events follow the same convention of as the second type. For instance, if an MT contract uses the [approval management standard](https://nomicon.io/Standards/MultiToken/ApprovalManagement.html), it may emit an event for `mt_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. + +## Drawbacks + +There is a known limitation of 16kb strings when capturing logs. +This can be observed from `token_ids` that may vary in length +for different apps so the amount of logs that can +be executed may vary. diff --git a/specs/Standards/MultiToken/Metadata.md b/specs/Standards/MultiToken/Metadata.md new file mode 100644 index 000000000..09b6237ea --- /dev/null +++ b/specs/Standards/MultiToken/Metadata.md @@ -0,0 +1,144 @@ +# Multi Token Metadata([NEP-245](https://github.com/near/NEPs/discussions/246)) + +:::caution +This is part of the proposed spec [NEP-245](https://github.com/near/NEPs/blob/master/neps/nep-0245.md) and is subject to change. +::: + +Version `1.0.0` + +## Summary + +An interface for a multi token's metadata. The goal is to keep the metadata future-proof as well as lightweight. This will be important to dApps needing additional information about multi token properties, and broadly compatible with other token standards such that the [NEAR Rainbow Bridge](https://near.org/blog/eth-near-rainbow-bridge/) can move tokens between chains. + +## Motivation + +The primary value of tokens comes from their metadata. While the [core standard](Core.md) provides the minimum interface that can be considered a multi token, most artists, developers, and dApps will want to associate more data with each token, and will want a predictable way to interact with any MT's metadata. + +NEAR's unique [storage staking](https://docs.near.org/docs/concepts/storage-staking) approach makes it feasible to store more data on-chain than other blockchains. This standard leverages this strength for common metadata attributes, and provides a standard way to link to additional offchain data to support rapid community experimentation. + +This standard also provides a `spec` version. This makes it easy for consumers of Multi Tokens, such as marketplaces, to know if they support all the features of a given token. + +Prior art: + +- NEAR's [Fungible Token Metadata Standard](../FungibleToken/Metadata.md) +- NEAR's [Non-Fungible Token Metadata Standard](../NonFungibleToken/Metadata.md) +- Discussion about NEAR's complete NFT standard: #171 +- Discussion about NEAR's complete Multi Token standard: #245 + +## Interface + +Metadata applies at both the class level (`MTBaseTokenMetadata`) and the specific instance level (`MTTokenMetadata`). The relevant metadata for each: + +```ts + +type MTContractMetadata = { + spec: string, // required, essentially a version like "mt-1.0.0" + name: string, // required Zoink's Digitial Sword Collection +} + +type MTBaseTokenMetadata = { + name: string, // required, ex. "Silver Swords" or "Metaverse 3" + id: string, // required a unique identifier for the metadata + symbol: string|null, // required, ex. "MOCHI" + icon: string|null, // Data URL + decimals: string|null // number of decimals for the token useful for FT related tokens + base_uri: string|null, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs + reference: string|null, // URL to a JSON file with more info + copies: number|null, // number of copies of this set of metadata in existence when token was minted. + reference_hash: string|null, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. +} + +type MTTokenMetadata = { + title: string|null, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055" + description: string|null, // free-form description + media: string|null, // URL to associated media, preferably to decentralized, content-addressed storage + media_hash: string|null, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included. + issued_at: string|null, // When token was issued or minted, Unix epoch in milliseconds + expires_at: string|null, // When token expires, Unix epoch in milliseconds + starts_at: string|null, // When token starts being valid, Unix epoch in milliseconds + updated_at: string|null, // When token was last updated, Unix epoch in milliseconds + extra: string|null, // Anything extra the MT wants to store on-chain. Can be stringified JSON. + reference: string|null, // URL to an off-chain JSON file with more info. + reference_hash: string|null // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. +} + +type MTTokenMetadataAll = { + base: MTBaseTokenMetadata + token: MTTokenMetadata +} +``` + +A new set of functions MUST be supported on the MT contract: + +```ts +// Returns the top-level contract level metadtata +function mt_metadata_contract(): MTContractMetadata {} +function mt_metadata_token_all(token_ids: string[]): MTTokenMetadataAll[] +function mt_metadata_token_by_token_id(token_ids: string[]): MTTokenMetadata[] +function mt_metadata_base_by_token_id(token_ids: string[]): MTBaseTokenMetadata[] +function mt_metadata_base_by_metadata_id(base_metadata_ids: string[]): MTBaseTokenMetadata[] + +``` + +A new attribute MUST be added to each `Token` struct: + +```diff + type Token = { + token_id: string, ++ token_metadata?: MTTokenMetadata, ++ base_metadata_id: string, + } +``` + +### An implementing contract MUST include the following fields on-chain +For `MTContractMetadata`: +- `spec`: a string that MUST be formatted `mt-1.0.0` to indicate that a Multi Token contract adheres to the current versions of this Metadata spec. This will allow consumers of the Multi Token to know if they support the features of a given contract. +- `name`: the human-readable name of the contract. + +### An implementing contract must include the following fields on-chain +For `MTBaseTokenMetadata`: +- `name`: the human-readable name of the Token. +- `base_uri`: Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs. Can be used by other frontends for initial retrieval of assets, even if these frontends then replicate the data to their own decentralized nodes, which they are encouraged to do. + +### An implementing contract MAY include the following fields on-chain +For `MTBaseTokenMetadata`: +- `symbol`: the abbreviated symbol of the contract, like MOCHI or MV3 +- `icon`: a small image associated with this contract. Encouraged to be a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs), to help consumers display it quickly while protecting user data. Recommendation: use [optimized SVG](https://codepen.io/tigt/post/optimizing-svgs-in-data-uris), which can result in high-resolution images with only 100s of bytes of [storage cost](https://docs.near.org/docs/concepts/storage-staking). (Note that these storage costs are incurred to the contract deployer, but that querying these icons is a very cheap & cacheable read operation for all consumers of the contract and the RPC nodes that serve the data.) Recommendation: create icons that will work well with both light-mode and dark-mode websites by either using middle-tone color schemes, or by [embedding `media` queries in the SVG](https://timkadlec.com/2013/04/media-queries-within-svg/). +- `reference`: a link to a valid JSON file containing various keys offering supplementary details on the token. Example: "/ipfs/QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm", "https://example.com/token.json", etc. If the information given in this document conflicts with the on-chain attributes, the values in `reference` shall be considered the source of truth. +- `reference_hash`: the base64-encoded sha256 hash of the JSON file contained in the `reference` field. This is to guard against off-chain tampering. +- `copies`: The number of tokens with this set of metadata or `media` known to exist at time of minting. Supply is a more accurate current reflection. + +For `MTTokenMetadata`: + +- `title`: The title of this specific token. +- `description`: A longer description of the token. +- `media`: URL to associated media. Preferably to decentralized, content-addressed storage. +- `media_hash`: the base64-encoded sha256 hash of content referenced by the `media` field. This is to guard against off-chain tampering. +- `copies`: The number of tokens with this set of metadata or `media` known to exist at time of minting. +- `issued_at`: Unix epoch in milliseconds when token was issued or minted (an unsigned 32-bit integer would suffice until the year 2106) +- `expires_at`: Unix epoch in milliseconds when token expires +- `starts_at`: Unix epoch in milliseconds when token starts being valid +- `updated_at`: Unix epoch in milliseconds when token was last updated +- `extra`: anything extra the MT wants to store on-chain. Can be stringified JSON. +- `reference`: URL to an off-chain JSON file with more info. +- `reference_hash`: Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included. + +For `MTTokenMetadataAll `: + +- `base`: The base metadata that corresponds to `MTBaseTokenMetadata` for the token. +- `token`: The token specific metadata that corresponds to `MTTokenMetadata`. + +### No incurred cost for core MT behavior + +Contracts should be implemented in a way to avoid extra gas fees for serialization & deserialization of metadata for calls to `mt_*` methods other than `mt_metadata*` or `mt_tokens`. See `near-contract-standards` [implementation using `LazyOption`](https://github.com/near/near-sdk-rs/blob/c2771af7fdfe01a4e8414046752ee16fb0d29d39/examples/fungible-token/ft/src/lib.rs#L71) as a reference example. + +## Drawbacks + +* When this MT contract is created and initialized, the storage use per-token will be higher than an MT Core version. Frontends can account for this by adding extra deposit when minting. This could be done by padding with a reasonable amount, or by the frontend using the [RPC call detailed here](https://docs.near.org/docs/develop/front-end/rpc#genesis-config) that gets genesis configuration and actually determine precisely how much deposit is needed. +* Convention of `icon` being a data URL rather than a link to an HTTP endpoint that could contain privacy-violating code cannot be done on deploy or update of contract metadata, and must be done on the consumer/app side when displaying token data. +* If on-chain icon uses a data URL or is not set but the document given by `reference` contains a privacy-violating `icon` URL, consumers & apps of this data should not naïvely display the `reference` version, but should prefer the safe version. This is technically a violation of the "`reference` setting wins" policy described above. + +## Future possibilities + +- Detailed conventions that may be enforced for versions. +- A fleshed out schema for what the `reference` object should contain. diff --git a/specs/Standards/MultiToken/README.md b/specs/Standards/MultiToken/README.md new file mode 100644 index 000000000..a593d3907 --- /dev/null +++ b/specs/Standards/MultiToken/README.md @@ -0,0 +1,7 @@ +## Multi Token Standard + +- [Multi Token Core](Core.md) +- [Multi Token Metadata](Metadata.md) +- [Multi Token Approval Management](ApprovalManagement.md) +- [Multi Token Enumeration](Enumeration.md) +- [Multi Token Events](Events.md)