Sec3 logo — Solana smart contract security firm
Back to Blog
Education

Solana programs Part 3: understanding Metaplex Token Metadata

Sec3 Research Team

The metaplex-token-metadata program is the backbone of NFT on Solana.

Imagine you’d like to create an NFT for your art, essentially you need to:

  1. upload your art (digitized) to a permanent storage
  2. create a token mint (i.e., a unique identifier) for your art on Solana
  3. mint one token owned by your wallet

In Step 2, the token mint is special — its supply is only one and it must be associated with information of your art (e.g., name, creator, where it is, etc).

Metaplex has created a specification for such “metadata”, which can be added to every (special) token mint by the token-metadata program.

In this article, we elaborate on the implementation details of token-metadata.

An NFT created on Metaplex

An NFT created on Metaplex An NFT created on Metaplex

How to Mint NFT with Token Metadata?

The token-metadata program has a good number of instructions (more than 20 in total). The following four are the most commonly used:

  • MetadataInstruction::CreateMetadataAccountV2
  • MetadataInstruction::CreateMasterEditionV3
  • MetadataInstruction::MintNewEditionFromMasterEditionViaToken
  • MetadataInstruction::UpdateMetadataAccountV2

A transaction log on the token-metadata program

> Invoking Token Metadata Program
  > Program log: Instruction: Create Metadata Accounts v2
  > Program log: Transfer 5616720 lamports to the new account
  > Invoking System Program
    > Program returned success
  > Program log: Allocate space for the account
  > Invoking System Program
    > Program returned success
  > Program log: Assign the account to the owning program
  > Invoking System Program
    > Program returned success
  > Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 36986 of 130208 compute units
  > Program returned success
> Invoking Token Metadata Program
  > Program log: V3 Create Master Edition

A transaction log on the token-metadata program

1. CreateMetadataAccountV2

First, create a metadata account by calling function process_create_metadata_accounts_v2 :

MetadataInstruction::CreateMetadataAccountV2(args: CreateMetadataAccountArgsV2) => {
    msg!("Instruction: Create Metadata Accounts v2");
    process_create_metadata_accounts_v2(
        program_id,
        accounts,
        args.data,
        allow_direct_creator_writes: false,
        args.is_mutable,

Note: Every metadata account is a PDA with the token mint (mint_info) as a part of the seeds:

let metadata_seeds: &[&[u8]; 3] = &[
    PREFIX.as_bytes(),
    program_id.as_ref(),
    mint_info.key.as_ref(),
];

The mint authority (mint_authority_info ) must be a signer.

The Metadata struct has ten fields:

pub struct Metadata {
    pub key: Key,
    pub update_authority: Pubkey,
    pub mint: Pubkey,
    pub data: Data,
    // Immutable, once flipped, all sales of this metadata are considered secondary.
    pub primary_sale_happened: bool,
    // Whether or not the data struct is mutable, default is not
    pub is_mutable: bool,
    /// nonce for easy calculation of editions, if present
    pub edition_nonce: Option<u8>,
    /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end.
    pub token_standard: Option<TokenStandard>,
    /// Collection
    pub collection: Option<Collection>,
    /// Uses
    pub uses: Option<Uses>,
}

The function basically initializes each field, e.g., metadata.mint, metadata.update_authority and metadata.data :

metadata.mint = *mint_info.key;
metadata.key = Key::MetadataV1;
metadata.data = data.to_v1();
metadata.is_mutable = is_mutable;
metadata.update_authority = update_authority_key;

The metadata.data records the NFT’s attributes (e.g., name, uri and creators):

pub struct DataV2 {
    /// The name of the asset
    pub name: String,
    /// The symbol for the asset
    pub symbol: String,
    /// URI pointing to JSON representing the asset
    pub uri: String,
    /// Royalty basis points that goes to creators in secondary sales (0-10000)
    pub seller_fee_basis_points: u16,
    /// Array of creators, optional
    pub creators: Option<Vec<Creator>>,
    /// Collection
    pub collection: Option<Collection>,
    /// Uses
    pub uses: Option<Uses>,
}

This metadata account will serve as the master_metadata of the NFT**.**

2. CreateMasterEditionV3

Then, create a master edition account of the NFT, so that you can use it to print multiple editions (e.g., a limited supply of 10) of your NFT later.

MetadataInstruction::CreateMasterEditionV3(args: CreateMasterEditionArgs) => {
    msg!("V3 Create Master Edition");
    process_create_master_edition(program_id, accounts, args.max_supply)

The function process_create_master_edition is used to create a master edition account (edition_account_info ) for a token mint given a max_supply :

pub fn process_create_master_edition(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    max_supply: Option<u64>,
) -> ProgramResult {
    let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();

    let edition_account_info: &AccountInfo = next_account_info(account_info_iter)?;
    let mint_info: &AccountInfo = next_account_info(account_info_iter)?;
    let update_authority_info: &AccountInfo = next_account_info(account_info_iter)?;

Note1: Every master edition account is a PDA with the token mint (mint_info) as a part of the seeds:

let edition_authority_seeds: &[&[u8]; 5] = &[
    PREFIX.as_bytes(),
    program_id.as_ref(),
    &mint_info.key.as_ref(),
    EDITION.as_bytes(),
    &[bump_seed],

Note2: The function can only be called once for a token mint by the update_authority of the token metadata, and only if the supply on the mint is one:

if mint.supply != 1 {
    return Err(MetadataError::EditionsMustHaveExactlyOneToken.into());
}

The MasterEdition record has three fields:

pub struct MasterEditionV2 {
pub key: Key,
pub supply: u64,
pub max_supply: Option,}

The function basically initializes these fields:

edition.key = Key::MasterEditionV2;
edition.supply = 0;
edition.max_supply = max_supply;

Finally, it transfers the authority of the token mint (AuthorityType::MintTokensand AuthorityType::FreezeAccount ) to the master edition account:

// While you can't mint any more of your master record, you can
// mint as many limited editions as you like within your max supply.
transfer_mint_authority(
    edition_key: edition_account_info.key,
    edition_account_info,
    mint_info,
    mint_authority_info,
    token_program_info,

After that, other token mints may become editions of this metadata, representing limited editions of the NFT.

3. MintNewEditionFromMasterEditionViaToken

The function process_mint_new_edition_from_master_edition_via_token is used to mint new editions of an NFT from its master edition:

MetadataInstruction::MintNewEditionFromMasterEditionViaToken(args: MintNewEditionFromMasterEditionViaTokenArgs) => {
    msg!("Instruction: Mint New Edition from Master Edition Via Token");
    process_mint_new_edition_from_master_edition_via_token(
        program_id,
        accounts,
        args.edition,
        ignore_owner_signer: false,

The caller provides a new mint account and an edition number, and must be signer of the master_metadata.mint’s token account (i.e., an owner of the NFT):

let master_metadata: Metadata = Metadata::from_account_info(master_metadata_account_info)?;
let token_account: Account = assert_initialized(token_account_info)?;

if !ignore_owner_signer {
    assert_signer(owner_account_info)?;

    if token_account.owner != *owner_account_info.key {
        return Err(MetadataError::InvalidOwner.into());
    }
}

if token_account.mint != master_metadata.mint {
    return Err(MetadataError::TokenAccountMintMismatchV2.into());
}

if token_account.amount < 1 {
    return Err(MetadataError::NotEnoughTokens.into());

The function will create a new edition_marker PDA using the edition number and the NFT’s mint (master_metadata.mint) as a part of the seeds:

let edition_number: u64 = edition.checked_div(EDITION_MARKER_BIT_SIZE).unwrap();
let as_string: String = edition_number.to_string();

let bump: u8 = assert_derivation(
    program_id,
    account: edition_marker_info,
    path: &[
        PREFIX.as_bytes(),
        program_id.as_ref(),
        master_metadata.mint.as_ref(),
        EDITION.as_bytes(),
        as_string.as_bytes(),

Then, the function mint_limited_edition is called to mint the new edition:

mint_limited_edition(
    program_id,
    master_metadata,
    new_metadata_account_info,
    new_edition_account_info,
    master_edition_account_info,
    mint_info,
    mint_authority_info,
    payer_account_info,
    update_authority_info,
    token_program_account_info,
    system_account_info,
    rent_info,
    reservation_list_info: None,
    edition_override: Some(edition),

In mint_limited_edition , a new metadata account will be created based on the new mint account (mint_info):

process_create_metadata_accounts_logic(
    &program_id,
    accounts: CreateMetadataAccountsLogicArgs {
        metadata_account_info: new_metadata_account_info,
        mint_info,
        mint_authority_info,
        payer_account_info,
        update_authority_info,
        system_account_info,
        rent_info,
    },
    data_v2,
    allow_direct_creator_writes: true,
    is_mutable: false,
    is_edition: true,
    add_token_standard: true,

The new mint must be different from master_metadata.mint and its supply must be one:

if mint_supply != 1 {
    return Err(MetadataError::EditionsMustHaveExactlyOneToken.into());

The new mint account is also part of the seeds for creating the new edition PDA:

let edition_seeds: &[&[u8]; 4] = &[
    PREFIX.as_bytes(),
    program_id.as_ref(),
    &mint_info.key.as_ref(),
    EDITION.as_bytes(),
];
let (edition_key: Pubkey, bump_seed: u8) = Pubkey::find_program_address(edition_seeds, program_id);
if edition_key != *new_edition_account_info.key {
    return Err(MetadataError::InvalidEditionKey.into());

Finally, similar to that in CreateMasterEditionV3 , the new mint account’s authority (AuthorityType::MintTokens and AuthorityType::FreezeAccount ) is set to the new edition account:

// Now make sure this mint can never be used by anybody else.
transfer_mint_authority(
    &edition_key,
    new_edition_account_info,
    mint_info,
    mint_authority_info,
    token_program_info: token_program_account_info,

4. UpdateMetadataAccountV2

The function process_update_metadata_accounts_v2 can be used to update the metadata at any time by the metadata’s update_authority:

MetadataInstruction::UpdateMetadataAccountV2(args: UpdateMetadataAccountArgsV2) => {
    msg!("Instruction: Update Metadata Accounts v2");
    process_update_metadata_accounts_v2(
        program_id,
        accounts,
        optional_data: args.data,
        args.update_authority,
        args.primary_sale_happened,
        args.is_mutable,

To update the metadata.data record, metadata.is_mutable must be true :

assert_update_authority_is_correct(&metadata, update_authority_info)?;

if let Some(data: DataV2) = optional_data {
    if metadata.is_mutable {
        let compatible_data: Data = data.to_v1();
        assert_data_valid(
            &compatible_data,
            update_authority: update_authority_info.key,
            existing_metadata: &metadata,
            allow_direct_creator_writes: false,
            update_authority_is_signer: update_authority_info.is_signer,
            is_updating: true,
        )?;
        metadata.data = compatible_data;
        assert_collection_update_is_valid(edition: false, _existing: &metadata.collection, incoming: &data.collection)?;
        metadata.collection = data.collection;

The metadata’s update_authority can also be updated, as well as metadata.primary_sale_happened :

if let Some(val: Pubkey) = update_authority {
    metadata.update_authority = val;
}

if let Some(val: bool) = primary_sale_happened {
    if val {
        metadata.primary_sale_happened = val
    } else {
        return Err(MetadataError::PrimarySaleCanOnlyBeFlippedToTrue.into());

Other Token Metadata instructions

The token-metadata program has several other functionalities such as verifying the NFT creators (see full code):

MetadataInstruction::SignMetadata => {
    msg!(“Instruction: Sign Metadata”);
    process_sign_metadata(program_id, accounts)}
MetadataInstruction::RemoveCreatorVerification => {
    msg!("Instruction: Remove Creator Verification");
    process_remove_creator_verification(program_id, accounts)}

In the next few articles, we will continue to highlight the technical details of a few more popular Solana programs.


Sec3 (formerly Soteria) Auto Auditor Software and Security Audit Service

Sec3 is founded by leading minds in the fields of blockchain security and software verification.

We are pleased to provide an automatic security scanner software and security audit services to high-impact Dapps on Solana. Please visit Sec3.dev for more information.

Related Posts

Education

Solana Programs Part 4

The Metaplex Candy Machine is among the most popular smart contracts used for NFT minting on Solana. Recently, it has even implemented sophisticated logic for detecting and taxing bots. How does the candy machine program work internally? What are its intended use cases and dependencies? How does it detect bots? This article elaborates on these technical details.

Read more
Education

Solana Programs Part 1

Most user-deployed Solana smart contracts (directly or transitively) use the token program to mint/transfer/burn tokens (i.e., SPL tokens). SPL tokens are similar to ERC20/ERC721 token with tricky differences. In this article, we elaborate on the SPL tokens and introduce the internals of the most commonly used instructions in the token program.

Read more