sRFC 00017: Token Metadata Interface

Token-Metadata Interface

Summary

Token-metadata is a very complex space, but at its base, all creators of
fungible and non-fungible tokens need a way to upload information about their token
on-chain. This proposal contains a spec for a simple token-metadata state
and instruction interface for SPL token mints. The interface can be implemented
by any program.

With a common interface, any wallet, dapp, or on-chain program can read the metadata,
and any tool that creates or modifies metadata will just work with any program
that implements the interface.

Motivation

Token creators on Solana need all sorts of functionality for their token-metadata,
and the Metaplex Token-Metadata program has been the one place for all metadata
needs, leading to a feature-rich program that still might not serve all needs.

At its base, token-metadata is a set of data fields associated to a particular token
mint, so we propose an interface that serves the simplest base case with some
compatibility with existing solutions.

With this proposal implemented, fungible and non-fungible token creators will
have two options:

  • implement the interface in their own program, so they can eventually extend it
    with new functionality or even other interfaces
  • use a reference program that implements the simplest case

Spec

Token-Metadata Struct

A program that implements the interface must write the following data fields
into a type-length-value entry into the account:

type Pubkey = [u8; 32];
type OptionalNonZeroPubkey = Pubkey; // if all zeroes, interpreted as `None`
type TlvDiscriminator = [u8; 8];

struct TokenMetadata {
    discriminator: TlvDiscriminator,
    length: u32,
    update_authority: OptionalNonZeroPubkey,
    mint: Pubkey,
    name_len: u32,
    name: [u8; 32],
    symbol_len: u32,
    symbol: [u8; 10],
    uri_len: u32,
    uri: [u8; 200],
}

This struct has some ABI-compatibility with the Metaplex
Metadata struct.
The discriminator here is larger, at 8 bytes instead of 1, and is a different
value, but the following 318 bytes can be interpreted in the same way by wallets,
programs, and indexers.

The discriminator must be hashv(&["token-metadata-interface::state"])[0..8].

By storing the metadata in a TLV structure, a developer who implements this
interface in their program can freely add any other data fields in a different
TLV entry.

You can find more information about TLV / type-length-value structures at the
spl-type-length-value repo.

Instructions

Here are the instructions that must be implemented by a program conforming to
the interface.

Initialize Token Metadata

  • Discriminator: hashv(&["token-metadata-interface::initialize"])[0..8]
  • Accounts:
    0. [writable] Metadata account
    1. [] SPL mint account
    2. [signer] Mint authority
    3. [] Update authority
  • Data:
    0. name: String
    1. symbol: String
    2. uri: String

The instruction processor must do the following:

  • check that the mint account is an SPL mint
  • check that the correct mint authority signed
  • check that the name / symbol / URI fit in the limits of the struct
  • check that the metadata account does not already have metadata written to it
  • write all of the information into the metadata account

NOTE: This instruction only covers initialization and assumes that the provided
account is properly created, meaning that it has enough space for the data, and
enough lamports to be rent-exempt.

Update Token Metadata

  • Discriminator: hashv(&["token-metadata-interface::update"])[0..8]
  • Accounts:
    0. [writable] Metadata account
    1. [signer] Update authority
  • Data:
    0. name: String
    1. symbol: String
    2. uri: String

The instruction processor must do the following:

  • check that the metadata account is owned by the program and contains a valid
    token-metadata TLV entry
  • check that the update authority signed
  • check that the name / symbol / URI fit in the limits of the struct
  • write the new information

NOTE: Strings are utf-8 encoded bytes, preceded by a little-endian u32
giving the number of bytes.

While these instructions aren’t strictly required to adhere to the interface,
they will integrate more nicely with other tooling.

For example, the JS SDK for token-interface can allow targeting any program id,
so if a program implements these instructions properly, then they can easily get
more usage.

Alternatives

As described at the start of this proposal, the space of token-metadata is vast
and comprises all sorts of functionality, including fees, programmability,
transferability, minting, etc.

This proposal is deliberately not specific to fungible or non-fungible tokens,
so it includes the base required for both, and nothing more.

For example, functionality for royalties could be implemented through a separate
interface.

(Optional) Program-Derived Address Convention

This base proposal only defines the functionality required to implement the interface,
and does not define any program-derived addresses for where the metadata should
be stored.

This approach is deliberate: by not requiring a particular address derivation,
it is possible for a token program to also implement the metadata interface!

On the other hand, since many metadata programs will likely not be token
programs, we include an optional convention for deriving a program address for
metadata as the following function call:

use solana_program::pubkey::Pubkey;
pub fn metadata_account_address(metadata_program_id: &Pubkey, mint: &Pubkey) -> Pubkey {
    Pubkey::find_program_address(&[b"metadata", mint.as_ref()], metadata_program_id).0
}

Point for discussion: this is different from the Metaplex token-metadata derivation,
which repeats the metadata program id. This approach feels reasonable, given that
this interface already isn’t fully compatible with the Metaplex token-metadata accounts.

Client libraries need to parse Metaplex token-metadata accounts differently from
accounts that implement the token-metadata interface anyway, so special-casing
the address derivation for Metaplex seems reasonable.

On the flip-side, we can also completely omit this point and leave it for
another proposal regarding “token-metadata discoverability”, which may include an
interface for a token-metadata registry.

Further Work

This interface defines the minimum struct and instructions that a program must
implement in order to be considered a “token-metadata program”. It does not address
discoverability of token-metadata accounts.

For discoverability within the mint, spl-token-2022 will add a mint “extension”
to store the metadata account address.

As a proof-of-concept, spl-token-2022 will also implement this interface, and store the
metadata fields directly in the mint account.

9 Likes

I love this. Could the interface also include an optional list of traits? That would be great for dynamic nfts and games for example. I think it could open lots of possibilities.

6 Likes

This is great, Jon!

So Token2022 will demonstrate a program adhering to the metadata interface by:

  • Implementing the required instructions in extension(s)
  • Using its Mint account to also store Metadata state

For anyone who might be confused, Token2022 is implementing this interface through extensions, but that particular way of implementing interfaces is specific to Token2022, and you can choose to implement the instructions/state however you see fit, so long as everything checks out with things like instructions, required accounts, discriminators, etc.

As Jon mentioned above, something we still need to hash out going forward is the discoverability of PDA accounts with Metadata state, when devs choose not to pack both states into the Mint.

I’m assuming we wouldn’t want to set any kind of standard for seeds (ie. “your PDA must be derived like this”)? This would make discoverability easier, but inhibit flexibility for devs.

An alternative might be Anchor-like discriminators - since we’re already enforcing a discriminator for the instructions?

3 Likes

I think the idea with interfaces - especially when it comes to Token Metadata - is to have separate, modular interfaces for every “new” type of metadata or “extension” on metadata.

For example, you might implement:

  • Jon’s interface above for the base Metadata (name, symbol, uri)
  • Jonas’s interface for traits (traits, etc.)
  • *optionally any other interface you want

And then that’s how you can build expanded metadata, and you can choose whether or not to follow Metaplex’s idea of PDAs or pack it all into one state

5 Likes

Can you describe these optional traits more? Do you mean something like “my token has some base metadata, but it also has other metadata that can change”?

If that’s the case, my guess is that they should be done through another interface, as Joe mentioned, since this is just for token metadata. But if you have a more complete view about how this can fit in, please let me know!

2 Likes

I’m assuming we wouldn’t want to set any kind of standard for seeds (ie. “your PDA must be derived like this”)? This would make discoverability easier, but inhibit flexibility for devs.

We could! That would be part of the interface, especially where it makes sense. For token metadata, basing it off the mint address seems like a slam dunk. For more complicated cases like trading programs though, it would probably inhibit flexibility if you had to use just a mint address, and not a user wallet, for example.

An alternative might be Anchor-like discriminators - since we’re already enforcing a discriminator for the instructions?

Do you mean discriminators for seeds? We could certainly do that! On the flipside, since Pubkey::find_program_address is already hashing everything together, a byte-string would probably be simplest and also most flexible since you’re not beholden to any particular size.

3 Likes

Hey @joncinque
WRT my comment about changing the data layout I think in order to gain more flexibility the data layout needs to support any size of string uri and custom schematized content blocks .
This can be done by having a fixed size header and then manually deserializing blocks of typed data after the header. View functions on the rust or ts libs that allow a user to grab the specific content blocks that relate to uri or on chain full metadata or what have you.

I dont see the point of this unless its better than token metadata by alot. Unless you allow the developers to do more than they can now then this may not succeed . We also know that “composability” sucks in practice when you need to compose several accounts together to get a single entity. So allowing custom data blocks to be added will allow more use cases that other interfaces can describe like a traits interface, grouping interface, multi owner interface etcetera

1 Like

tl;dr Proposal is missing indexing standard

If this is optional, how are indexers / wallet supposed to show metadata for programs that adhere to this sRFC?

One of the main challenges for Metadata programs is that they will never be shown in wallets.
The main benefit of something like a Metadata spec should be giving each metadata program equal opportunity to be shown in a wallet, which I imagine would require some form of indexing standard as well.

I think this makes sense, but this could also be extremely cost ineffective for large metadata collections.

Another missing item is the ability to support indexing by the following:

  • collection/grouping ID,
  • owner
  • delegate
  • creators

These are crucial to marketplace functionality that drives the economic usefulness of the already existing token-metadata program.

To be frank, I think this is useful metadata interface for tokens but without the indexing standard, I don’t think it’s useful for NFTs.

2 Likes

That makes sense. I’ll update the proposal to reflect something more general. I’m thinking a Vec<(String, String)> which allows for key-value pairs. I worry that other types would overcomplicate the proposal, and ultimately go against the core of the proposal, which is storing non-functional data.

If an account has additional functional data, like multiple owners, then it’s no longer metadata, and should be accomplished through a different interface. Thanks to TLV data structures, it’s possible to store all of these in the same account, but to get different views on the same account.

But thankfully, if people want to violate that, they can always store JSON strings :melting_face:

2 Likes

We discussed this offline, and the solution we came up with was to add a view function which emits the data as an event, and to mark the “state” portion of the interface as optional, so that programs can have maximum flexibility in the implementation.

Programs that omit the “state” portion of the interface may face more integration challenges with wallets or programs that read the account data without a simulation or CPI.

5 Likes

I plan to review this more in-depth but from first glance uri_len can be a u8

2 Likes

Note this has been implemented in https://github.com/solana-labs/solana-program-library/tree/master/token-metadata/interface along with an example program at https://github.com/solana-labs/solana-program-library/tree/master/token-metadata/example and an implementation in token-2022 at https://github.com/solana-labs/solana-program-library/tree/master/token/program-2022/src/extension/token_metadata, so feel free to put in an issue to SPL if you have any additional comments!

2 Likes

Appreciate this a lot! Glad to see its implemented and will check the example out.

austbot
Hey @joncinque
WRT my comment about changing the data layout I think in order to gain more flexibility the data layout needs to support any size of string uri and custom schematized content blocks.

Yes this is extremely important! For example: with an increased uri size of 3-4000 characters, we can essentially have whole NFT metadata (JSON with traits and images) on-chain, within the same account. That size fits SMB’s, Tensorians, etc. and of course Blockrons.

3 Likes

Hey everyone! I just proposed a “Field Authority Interface (sRFC 23)” that works alongside this, check it out!

1 Like

After working with clients to adopt this, I’d recommend pivoting to just parsing account data as the standard read method. emit() requires running a transaction with a signer – clients that want to just read will therefore need to audit the program first, making it harder for new programs to get adopted.

cc @ngundotra

2 Likes

The idea of emit() is to use it with transaction simulation or as part of a running transaction, and not as a standalone instruction, unless you wanted to maintain proof of the state of an account at a certain point in time. Although reading the bytes from the account is easier, not all programs need to support it, which is the other reason to have the instruction.

3 Likes

Ahhh that makes sense, got it.

3 Likes