Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multi Token Contract Standard #246

Open
zcstarr opened this issue Jul 28, 2021 · 13 comments
Open

Multi Token Contract Standard #246

zcstarr opened this issue Jul 28, 2021 · 13 comments
Labels
WG-contract-standards Contract Standards Work Group should be accountable

Comments

@zcstarr
Copy link
Contributor

zcstarr commented Jul 28, 2021

Summary

A standard interface for a multi token standard that supports fungible, semi-fungible, and tokens of any type, allowing for ownership, transfer, and batch transfer of tokens generally regardless of specific type. This standard aims to make it possible to represent these multi tokens in a way that allows these tokens to be represented across chains.

Motivation

Having a single contract represent both NFTs and FTs can greatly improve efficiency as demonstrated by Enjin Coin. The ability to make batch requests with multiple asset classes can reduce a many transactions, transaction to a single transaction to trade around both NFTs and FTs that are a part of same token contract. Moreover, this flexibility has led to building proper infrastructure for ecosystems, that often have to issue many tokens under management such as games.

Finally on other chains, such as Ethereum it would be nice to be able to bridge a token contract from near to ETH or to other chains. Having a multi token standard that can represent many different classes of tokens is highly desirable

Background

Core Trait

pub trait MultiTokenCore {
    /// Basic token transfer. Transfer a token or tokens given a token_id. The token id can correspond to  
    /// either a NonFungibleToken or Fungible Token this is differeniated by the implementation.
    ///
    /// Requirements
    /// * Caller of the method must attach a deposit of 1 yoctoⓃ for security purposes
    /// * Contract MUST panic if called by someone other than token owner or,
    /// * If using Approval Management, contract MUST nullify approved accounts on
    ///   successful transfer.
    /// * TODO: needed? Both accounts must be registered with the contract for transfer to
    ///   succeed. See see https://nomicon.io/Standards/StorageManagement.html
    ///
    /// Arguments:
    /// * `receiver_id`: the valid NEAR account receiving the token
    /// * `token_id`: the token or tokens to transfer
    /// * `amount`: the token amount of tokens to transfer for token_id
    /// * `memo` (optional): for use cases that may benefit from indexing or
    ///    providing information for a transfer
    fn mt_transfer(
        &mut self,
        receiver_id: AccountId,
        token_id: TokenId,
        amount: U128,
        memo: Option<String>,
    );

    /// Transfer token/s and call a method on a receiver contract. A successful
    /// workflow will end in a success execution outcome to the callback on the MultiToken
    /// contract at the method `mt_resolve_transfer`.
    ///
    /// You can think of this as being similar to attaching  tokens to a
    /// function call. It allows you to attach any Fungible or Non Fungible Token in a call to a
    /// receiver contract.
    ///
    /// Requirements:
    /// * Caller of the method must attach a deposit of 1 yoctoⓃ for security
    ///   purposes
    /// * Contract MUST panic if called by someone other than token owner or,
    ///   if using Approval Management, one of the approved accounts
    /// * The receiving contract must implement `mt_on_transfer` according to the
    ///   standard. If it does not, MultiToken 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`
    ///
    /// Arguments:
    /// * `receiver_id`: the valid NEAR account receiving the token.
    /// * `token_id`: the token to send.
    /// * `amount`: amount of tokens to transfer for token_id
    /// * `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.
    fn mt_transfer_call(
        &mut self,
        receiver_id: AccountId,
        token_id: TokenId,
        amount: U128,
        memo: Option<String>,
        msg: String,
    ) -> PromiseOrValue<U128>;

    /// Batch token transfer. Transfer a tokens given token_ids and amounts. The token ids can correspond to  
    /// either Non-Fungible Tokens or Fungible Tokens or some combination of the two. The token ids
    /// are used to segment the types on a per contract implementation basis.
    ///
    /// Requirements
    /// * Caller of the method must attach a deposit of 1 yoctoⓃ for security purposes
    /// * Contract MUST panic if called by someone other than token owner or,
    ///   if using Approval Management, one of the approved accounts
    /// * `approval_id` is for use with Approval Management,
    ///   see https://nomicon.io/Standards/NonFungibleToken/ApprovalManagement.html
    /// * If using Approval Management, contract MUST nullify approved accounts on
    ///   successful transfer.
    /// * TODO: needed? Both accounts must be registered with the contract for transfer to
    ///   succeed. See see https://nomicon.io/Standards/StorageManagement.html
    /// * The token_ids vec and amounts vec must be of equal length and equate to a 1-1 mapping
    ///   between amount and id. In the event that they do not line up the call should fail
    ///
    /// Arguments:
    /// * `receiver_id`: the valid NEAR account receiving the token
    /// * `token_ids`: the tokens to transfer
    /// * `amounts`: the amount of tokens to transfer for corresponding token_id
    /// * `approval_ids`: expected approval ID. A number smaller than
    ///    2^53, and therefore representable as JSON. See Approval Management
    ///    standard for full explanation. Must have same length as token_ids
    /// * `memo` (optional): for use cases that may benefit from indexing or
    ///    providing information for a transfer

    fn mt_batch_transfer(
        &mut self,
        receiver_id: AccountId,
        token_ids: Vec<TokenId>,
        amounts: Vec<U128>,
        memo: Option<String>,
    );
    /// Batch transfer token/s and call a method on a receiver contract. A successful
    /// workflow will end in a success execution outcome to the callback on the MultiToken
    /// contract at the method `mt_resolve_batch_transfer`.
    ///
    /// You can think of this as being similar to attaching  tokens to a
    /// function call. It allows you to attach any Fungible or Non Fungible Token in a call to a
    /// receiver contract.
    ///
    /// Requirements:
    /// * Caller of the method must attach a deposit of 1 yoctoⓃ for security
    ///   purposes
    /// * Contract MUST panic if called by someone other than token owner or,
    ///   if using Approval Management, one of the approved accounts
    /// * The receiving contract must implement `mt_on_transfer` according to the
    ///   standard. If it does not, MultiToken contract's `mt_resolve_batch_transfer` MUST deal
    ///   with the resulting failed cross-contract call and roll back the transfer.
    /// * Contract MUST implement the behavior described in `mt_resolve_batch_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_ids`: the tokens to transfer
    /// * `amounts`: the amount of tokens to transfer for corresponding token_id
    /// * `approval_ids`: expected approval IDs. A number smaller than
    ///    2^53, and therefore representable as JSON. See Approval Management
    ///    standard for full explanation. Must have same length as token_ids
    /// * `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.

    fn mt_batch_transfer_call(
        &mut self,
        receiver_id: AccountId,
        token_ids: Vec<TokenId>,
        amounts: Vec<U128>,
        memo: Option<String>,
        msg: String,
    ) -> PromiseOrValue<Vec<U128>>;

    /// Get the balance of an an account given token_id. For fungible token returns back amount, for
    /// non fungible token it returns back constant 1.
    fn balance_of(&self, owner_id: AccountId, token_id: TokenId) -> U128;

    /// Get the balances of an an account given token_ids. For fungible token returns back amount, for
    /// non fungible token it returns back constant 1. returns vector of balances corresponding to token_ids
    /// in a 1-1 mapping
    fn balance_of_batch(&self, owner_id: AccountId, token_ids: Vec<TokenId>) -> Vec<U128>;

    /// Returns the total supply of the token in a decimal string representation given token_id.
    fn total_supply(&self, token_id: TokenId) -> U128;

    // Returns the total supplies of the tokens given by token_ids in a decimal string representation.
    fn total_supply_batch(&self, token_ids: Vec<TokenId>) -> Vec<U128>;
}

Receiver Trait

Notes

  • TokenId is of type String
pub trait MultiTokenReceiver {
    /// Take some action after receiving a MultiToken-tokens token
    ///
    /// Requirements:
    /// * Contract MUST restrict calls to this function to a set of whitelisted MultiToken
    ///   contracts
    ///
    /// Arguments:
    /// * `sender_id`: the sender of `mt_transfer_call`
    /// * `previous_owner_id`: 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`
    /// * `msg`: information necessary for this contract to know how to process the
    ///   request. This may include method names and/or arguments.
    ///
    /// Returns true if tokens should be returned to `sender_id`
    fn mt_on_transfer(
        &mut self,
        sender_id: AccountId,
        token_ids: Vec<TokenId>,
        amounts: Vec<U128>,
        msg: String,
    ) -> PromiseOrValue<Vec<U128>>;
}

Resolver Trait

Notes

  • TokenId is of type String
/// Used when MultiTokens are transferred using `mt_transfer_call`. This is the method that's called after `mt_on_transfer`. This trait is implemented on the MultiToken contract.
pub trait MultiTokenResolver {
    /// Finalize an `mt_transfer_call` chain of cross-contract calls.
    ///
    /// The `mt_transfer_call` process:
    ///
    /// 1. Sender calls `mt_transfer_call` on MultiToken contract
    /// 2. MultiToken contract transfers token from sender to receiver
    /// 3. MultiToken contract calls `mt_on_transfer` on receiver contract
    /// 4+. [receiver contract may make other cross-contract calls]
    /// N. MultiToken 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:
    /// * `previous_owner_id`: the owner prior to the call to `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`
    /// * `approvals`: if using Approval Management, contract MUST provide
    ///   set of original approved accounts in this argument, and restore these
    ///   approved accounts in case of revert. In this case it may be multiple sets of approvals
    ///
    /// Returns true if tokens were successfully transferred to `receiver_id`.
    fn mt_resolve_transfer(
        &mut self,
        sender_id: AccountId,
        receiver_id: AccountId,
        token_ids: Vec<TokenId>,
        amounts: Vec<U128>,
    ) -> Vec<U128>;
}

Storage Management Trait

Notes

This is semi necessary for ft token types to be able to refund users for storage of many different token types like gold/silver... this might be slightly out of scope

pub trait StorageManagement {
    // if `registration_only=true` MUST refund above the minimum balance if the account didn't exist and
    //     refund full deposit if the account exists.
    fn storage_deposit(
        &mut self,
        token_ids: Vec<TokenId>,
        account_id: Option<AccountId>,
        registration_only: Option<bool>,
    ) -> StorageBalance;

    /// Withdraw specified amount of available Ⓝ for predecessor account.
    ///
    /// This method is safe to call. It MUST NOT remove data.
    ///
    /// `amount` is sent as a string representing an unsigned 128-bit integer. If
    /// omitted, contract MUST refund full `available` balance. If `amount` exceeds
    /// predecessor account's available balance, contract MUST panic.
    ///
    /// If predecessor account not registered, contract MUST panic.
    ///
    /// MUST require exactly 1 yoctoNEAR attached balance to prevent restricted
    /// function-call access-key call (UX wallet security)
    ///
    /// Returns the StorageBalance structure showing updated balances.
    fn storage_withdraw(&mut self, token_ids:Vec<TokenId>, amount: Option<U128>) -> StorageBalance;

    /// Unregisters the predecessor account and returns the storage NEAR deposit back.
    ///
    /// If the predecessor account is not registered, the function MUST return `false` without panic.
    ///
    /// If `force=true` the function SHOULD ignore account balances (burn them) and close the account.
    /// Otherwise, MUST panic if caller has a positive registered balance (eg token holdings) or
    ///     the contract doesn't support force unregistration.
    /// MUST require exactly 1 yoctoNEAR attached balance to prevent restricted function-call access-key call
    /// (UX wallet security)
    /// Returns `true` iff the account was unregistered.
    /// Returns `false` iff account was not registered before.
    fn storage_unregister(&mut self, token_ids:Vec<TokenId>, force: Option<bool>) -> Vec<bool>;

    fn storage_balance_bounds(&self, token_id:TokenId, account_id: Option<AccountId>) -> StorageBalanceBounds;
    fn storage_balance_bounds_batch(&self, token_id:Vec<TokenId>, account_id: Option<AccountId>) -> StorageBalanceBounds;

    fn storage_balance_of(&self, token_id:TokenId, account_id: AccountId) -> Option<StorageBalance>;
    fn storage_balance_of_batch(&self, token_ids:Vec<TokenId>, account_id: AccountId) -> Option<StorageBalance>;
}

Metadata Trait

pub struct MultiTokenMetadata {
    pub spec: String,              // required, essentially a version like "mt-1.0.0"
    pub name: String,              // required, ex. "Mosaics"
    pub symbol: String,            // required, ex. "MOSIAC"
    pub icon: Option<String>,      // Data URL
    pub base_uri: Option<String>, // Centralized gateway known to have reliable access to decentralized storage assets referenced by `reference` or `media` URLs
    // supports metadata_uri interface that interpolates {id} in the string
    pub reference: Option<String>, // URL to a JSON file with more info
    pub reference_hash: Option<Base64VecU8>, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}

pub struct MultiTokenExtraMetadata {
    pub title: Option<String>, // ex. "Arch Nemesis: Mail Carrier" or "Parcel #5055"
    pub description: Option<String>, // free-form description
    pub media: Option<String>, // URL to associated media, preferably to decentralized, content-addressed storage
    pub media_hash: Option<Base64VecU8>, // Base64-encoded sha256 hash of content referenced by the `media` field. Required if `media` is included.
    pub copies: Option<u64>, // number of copies of this set of metadata in existence when token was minted.
    pub issued_at: Option<String>, // ISO 8601 datetime when token was issued or minted
    pub expires_at: Option<String>, // ISO 8601 datetime when token expires
    pub starts_at: Option<String>, // ISO 8601 datetime when token starts being valid
    pub updated_at: Option<String>, // ISO 8601 datetime when token was last updated
    pub extra: Option<String>, // anything extra the NFT wants to store on-chain. Can be stringified JSON.
    pub reference: Option<String>, // URL to an off-chain JSON file with more info.
    pub reference_hash: Option<Base64VecU8>, // Base64-encoded sha256 hash of JSON from reference field. Required if `reference` is included.
}

/// Offers details on the  metadata.
pub trait MultiTokenMetadataProvider {
    fn mt_metadata(&self, token_id: TokenId) -> MultiTokenMetadata;
    fn mt_extra_metadata(&self, token_id: TokenId) -> MultiTokenMetadataExtra;
}

PR #245

@Lev-Stambler
Copy link

Wait I need this and would love for this to come along!

@jorgeantonio21
Copy link

jorgeantonio21 commented Aug 3, 2022

Hey guys, could you provide updates on the status of the Multi Token Standard? This is a very much useful feature to have for the Near dev toolkit ! Thanks ! :)

@alexastrum
Copy link

alexastrum commented Oct 11, 2022

Hi, @zcstarr, @riqi, @jriemann, @marco-sundsk,
Is anyone from this list still interested in co-authoring?

I want to take over authorship and push to finalize this spec and its standard implementation.

@alexastrum
Copy link

alexastrum commented Oct 11, 2022

Hi @mikedotexe, @volovyks,
This PR shows Mike as the designated Reviewer for this NEP. Are you guys ok to review and push this into the Final Call, or is there someone else you'd assign to do this instead?

There are a few bugs in this NEP that I found when reviewing it with my friend @uncle-T0ny. We'd like to fix them and propose an implementation to be included in NEAR standards.

@frol frol added the WG-contract-standards Contract Standards Work Group should be accountable label Oct 12, 2022
@frol
Copy link
Collaborator

frol commented Oct 12, 2022

@alexastrum It would be awesome if you could sum up what work is left to be done. I see this draft implementation for near-sdk-rs: near/near-sdk-rs#776

@zcstarr
Copy link
Contributor Author

zcstarr commented Oct 12, 2022

@

Hi @mikedotexe, @volovyks, This PR shows Mike as the designated Reviewer for this NEP. Are you guys ok to review and push this into the Final Call, or is there someone else you'd assign to do this instead?

There are a few bugs in this NEP that I found when reviewing it with my friend @uncle-T0ny. We'd like to fix them and propose an implementation to be included in NEAR standards.

@alexastrum and @uncle-T0ny, would be great if you would post what you found as issues to the PR for the spec. or drop it here to open the convo up. I'd be curious to know what you hit. and would probably be in line with the new review process @frol ?

The original PR by @jriemann was done, but was unable to be merged in due to lack of time for anyone to actually review it.

@uncle-T0ny
Copy link

The list of the issues that I found:

  1. There is an approval invalidation issue: after transfer with approve, granted approve not become invalid, and grantee can make the transfers with the same approval again. Also, it's with the current reference implementation
    it's not possible to make a transfer on an amount, less than the approved.
  2. by spec, we have a method
function mt_is_approved(
  token_ids: [string],
  approved_account_id: string,
  amounts: [string],
  approval_ids: number[]|null
): boolean {}

and in the current implementation owner_id (an account that granted approval) we get from env::predecessor_account_id(), which means the method can't be viewable. As a solution, we should update the spec,
and add owner_id param to the args.
3. Storage management is not implemented for MT standard, to store tokens_per_owner, we need to cover the contract storage and (Storage Management spec NEP-145)[https://nomicon.io/Standards/StorageManagement] can be an appropriate solution. It means, before the transfer,a client should check the available balance by viewing storage_balance_of, and perform storage_deposit if required.

Proposals:

  • It would be nice to create a "Royalties and Payouts" spec as we have for the NFT token.

Based on near/near-sdk-rs#776 we're working on an updated version of the
ref. implementation that includes: fixes of mentioned above issues, near-sdk-rs updates with the relevant fixes, added more tests.

We would like to finalize this reference implementation and help the near community to receive the new standard. As soon as possible

@frol
Copy link
Collaborator

frol commented Oct 13, 2022

I want to take over authorship and push to finalize this spec and its standard implementation.

@alexastrum I think you can go ahead and prepare a PR, and this way we will continue the evolution of the standard. The best way to collaborate is to provide context and suggest solutions.

This PR shows Mike as the designated Reviewer for this NEP. Are you guys ok to review and push this into the Final Call, or is there someone else you'd assign to do this instead?

@near/wg-contract-standards will do the final review, but before we get there, the hope is to see a healthy discussion driven by the stakeholders of this standard.

@uncle-T0ny
Copy link

@frol, @zcstarr

This PR near/near-sdk-rs#950 is based on near/near-sdk-rs#776
with the updated codebase and the improvements.

The improvements list (by commits):

  1. near/near-sdk-rs@6d79045

We used near/near-sdk-rs#776 but in our forked near-sdk-rs because we needed to use the up-to-date codebase.

  1. near/near-sdk-rs@c22daf6
  • apply all the bugfixes in other standards that affect multi-token
  • update the MT by using a new way for XCC and using the "abi" feature
  1. near/near-sdk-rs@e7cabb4
  1. near/near-sdk-rs@438ec7a
  • fixed an approve invalidation bug, also decrease the approved amount after the transfer
  1. near/near-sdk-rs@8ff8d46
  • implement Storage Management for MT token, we think that it should be in the same way as for FT token, to cover the storage usage.
  • in some methods changed u128 to the wrapped version U128 for compatibility with the JS
  • improved e2e tests and added more cases
  • updated workspaces version

Could you please give us some advice on how to continue the MT finalization in the best way?

@blasrodri
Copy link
Contributor

As part of our bridge using IBC, we definitely need this standard to be merged. @frol

@frol
Copy link
Collaborator

frol commented Dec 20, 2022

(I am cross-posting this message for visibility from here: #245 (comment))

Hey folks, I want to give an update here as it seems that the work regarding this NEP is not visible in this PR.

Thanks to @uncle-T0ny, there is a solid PR with the implementation in near-sdk-rs (near/near-sdk-rs#950). The implementation was partially reviewed and @uncle-T0ny took it offline to test this implementation in his own contract before proceeding with submitting a NEP extension with the identified shortcomings to the current MT standard. Feel free to review the implementation and test it out!

@joshuajbouw
Copy link
Member

Excellent, we will be checking this out next quarter as we would like to help get this going and finalised. Will be playing around with it then.

@marco-sundsk
Copy link
Contributor

Now, we have a revision of this standard. This time, we focus on the combination of FT and NFT in those core functions cause we noticed some misunderstandings on FT and NFT workflow on near network in those interfaces. Beside that, we also make a supplement to the nep-0245.md to include all the extensions while leaving the one in specs untouched as a comparision base point, so that you could have a clear view about what have been changed in this revision.
To save your breath, we have a summary here to outline those key modifications and reasons behind them.

  • Make Token type completed.
    We add all possible items from those extensions (mainly metadata) with optional to make it neat as usual.
  • Remove owner_id from approval field in transfer related interfaces.
    For a FT, owner must be the sender. For a NFT, owner is not necessary to give in params, token contracts could find and only trust what they got from their on-chain status.
  • Move owner_id in approvals to the upper level and rename as previous_owner_ids in mt_resolve_transfer.
    Approvals is an extension to NFT, but previous_owner of the NFT is key to the interface otherwise the revert could NOT successfully work.
  • Move amount in approvals to the upper level in mt_resolve_transfer.
    Here, amounts is used here for FT, shouldn't be placed inside approval part.
  • Move copies from MTBaseTokenMetadata to MTTokenMetadata.
    Here copies usually collaberate with media to support such as multiple tokens with the same picture. It should stay with media.
  • Add base_id to MTTokenMetadataAll type to enable unified response of metadata queries.
    Normally, token contracts should return with full content in base field to save clients from extral query on base info. But in some special cases, base_id could be returned instead of base to shrink output size. It's token contract's call depending on their own situation.
  • Reduce the number of metadata related interfaces to 3.
    We think they are totally enough.
  • Remove amount in approval extension.
    On near, for FT, no approval is needed thanks to NEP-141. Meanwhile, NFT does care about amount. So, amount is not necessary in approval extension.

Now, the plan is to have an agreement on nep-0245.md first, then the corresponding fix would be applied to those in specs. And the standard code implementation would be the last.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WG-contract-standards Contract Standards Work Group should be accountable
Projects
None yet
Development

No branches or pull requests

10 participants