Candy-Machine

Candy-Machine

Overview

The Metaplex Candy Machine is a decentralized protocol built on Solana that enables creators to mint collections of NFT/SBTs in a controlled and trustless manner. It's one of the most popular tools in the Solana NFT ecosystem for managing large-scale NFT drops.

Key Features:

  1. Fair Minting: Candy Machine protects fairness by preventing bots from overwhelming the mint process. It allows for time-locked minting (launching at a specific time) and can limit the number of mints per wallet.
  1. Collection Management: Creators can choose to upload a whole collection of NFTs at once, which can be minted in sequential or random order. Or creators can use hidden setting, which means the NFTs data is not puiblic initially, creators can publish NFT data after mint and able to prove NFTs data hasn’t been tampered with.
  1. Custom Mint Settings: Creators can configure a variety of mint settings, such as:
      • Total supply of NFTs.
      • Start and end dates for minting.
      • Pricing for each mint.
      • Whitelists to give early access to specific users.

Workflow:

  1. Preparation: Creators upload the collection (art and metadata) to Arweave or IPFS for decentralized storage.
  1. Deployment: Deploy the Candy Machine with the collection and configure the mint parameters.
  1. Minting: Users interact with the Guard program to mint NFTs from the candy machine. Guard program ensures that mint operation reaches mint requirements such as fee, time range, whitelist, etc.
  1. Minted NFTs: Once minted, the NFTs are sent to the user's wallet.
The Metaplex Candy Machine simplifies the process of creating and managing NFT drops while ensuring transparency, fairness, and decentralized control.

Architecture

notion image

Components

Candy Machine Program

Candy machine program manages candy machine creation, NFT loading, NFT mint, etc.
Creator can use candy machine program to create a candy machine pda, which records information about this candy machine, such as the corresponding NFT collection, loaded NFTs information including names and urls.
Candy machine program handles NFT mint operation, including interacting with associated account program to create ATA for minters, and interacting with metaplex program to create NFT mint and metadata, etc.
Candy machine has one mint authority, only that authority account can use candy machine to mint NFT.

Guard Program

Guard Program manages guard creation, guard condition management.
If creator assign the mint authority to guard pda, then only guard program can mint NFT from candy machine.
Guard program supports a lot of conditions, like:
  1. Allow List: Uses a wallet address list to determine who is allowed to mint.
  1. Bot Tax: Configurable tax to charge invalid transactions.
  1. Sol Payment: Set the price of the mint in SOL.
  1. Start Date: Determines the start date of the mint.
  1. End Date: Determines a date to end the mint.

Process

  1. Creator calls candy machine prorgam to deploy candy machine
    1. Creator first creates a NFT collection mint.
    2. Creator interacts with candy machine program to create a candy machine(pda) which handles NFT mint of the specified NFT collection. Creator needs to specify the total NFTs count in this candy mahine.
      1. Note that in this process, the created candy machine owns delegate of the NFT collection, which means that it can verify that NFT it created belongs to the NFT collection.
    3. Creator can load NFTs. In this step, creator can choose two different solution. One is called config line setting, the creator needs to upload information of NFTs to storage service like Arweave or IPFS and get corresponding NFT metadata urls. Then creator loads all NFT’s names and urls to candy machine. This needs a lot space and corresponding rent. The other method is hidden setting where creator only specifies the prefix of NFT which will be stored in candy machine, and candy machine can concat NFT index to the same prefix to get each NFT’s name and url.
  1. Creator calls guard program to delpy guard pda and initialize guard conditions.
  1. Creator calls candy machine program to assign mint authority of candy machine to guard pda.
  1. User mints through guard program.

Tx & Address

Tx

Action
network
tx
create collection mint
devnet
create candy machine
devnet
insert config lines
devnet
mint through guard (third party signer)
devnet

Address

Name
Address
collectionUpdateAuthority
CvyWQvP9AKuB4inUNFwDkwhkMDxhkFZvHbD1eTU7ieHd
collectionMint
3UpLEGSBWVxY9C1qgvPbmiyzKp9PHCdr1zkmj4k2qjyV
Candy Machine Program
CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR
Guard Program
Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g

Create candy machine

In the tx, there are four instructions:
  1. create candy machine account
  1. Initialize candy machine
  1. Initialize guard
  1. Wrap
notion image

Create Candy Machine Data Account

It calls system account to create a candy machine data acocunt whose owner is candy machine program.

Candy Machine State

Fields:
  1. version
    1. V1: only creates NFT mint but doesn’t mint NFT
    2. V2: creates NFT mint and mint NFT to user
    3. Default version is version V2.
  1. token_standard:
    1. token standard of the NFT, such as NonFungible of ProgrammableNonFungible
  1. features
    1. for future functionality upgrade.
  1. authority
    1. authority which controls the created candy machine. It can insert NFT into candy machine, update shared NFT data, deletes candy machine, etc.
  1. collection_mint
    1. candy machine mints NFT under this NFT collection.
  1. items_redeemed
    1. how many NFTs have been minted.
  1. data
    1. basic information about candy machine and NFT
    2. items_available
      1. total available NFTs in the candy machine. If using config line setting, then only when all NFTs have been loaded, user can start minting.
    3. symbol
      1. symbol of created NFT.
    4. config_line_settings / hidden_settings
      1. records information of config line setting of hidffen setting. There is exactly one field that can be not None, and that is the setting this candy machine chooses.
 
Settings
  1. Config line setting
    1. In config line setting, candy machine considers each NFT has different name and url. And creators should upload each NFT’s name and url to candy machine. To save data space, creator can extract the common part of name and url and record those prefixes on blockchain. Also, in config line setting, creator can choose whether NFTs are minted with sequential or random index(pseudo-random).
  1. Hidden setting
    1. In hidden setting, candy machine considers that only index in name and url of all NFTs differ. So creator doesn’t need to upload each NFT’s name and url. Creator can set a common prefix, and inserts index place holder variable $ID$ or $ID+1$ into prefix. During mint process, candy machine automatically replace those place holder variable to actual index.
 
To support config line setting, candy machine prepares extra data space(hidden data section) to store related information:
  1. available NFTs count
    1. how many NFTs have been loaded.
  1. names and urls of all NFTs
    1. To keep a clear layout, each NFT’s name/url occupies same data size. The sizes of name and url are recorded in config_line_settings.name_length, config_line_settings.uri_length.
  1. bitmap tracks which NFT has been loaded
    1. when NFT are loaded, it will check the bitmap to find whether the NFT has been loaded before, it not, then it increases count records total loaded NFTs, also initializes its index in mint indices space which serves random mint.
/// --- programs/candy-machine-core/program/src/state/candy_machine.rs --- /// Candy machine state and config data. #[account] #[derive(Default, Debug)] pub struct CandyMachine { /// Version of the account. pub version: AccountVersion, /// Token standard to mint NFTs. pub token_standard: u8, /// Features flags. pub features: [u8; 6], /// Authority address. pub authority: Pubkey, /// Authority address allowed to mint from the candy machine. pub mint_authority: Pubkey, /// The collection mint for the candy machine. pub collection_mint: Pubkey, /// Number of assets redeemed. pub items_redeemed: u64, /// Candy machine configuration data. pub data: CandyMachineData, // hidden data section to avoid deserialisation: // // - (u32) how many actual lines of data there are currently (eventually // equals items available) // - (ConfigLine * items_available) lines and lines of name + uri data // - (item_available / 8) + 1 bit mask to keep track of which ConfigLines // have been added // - (u32 * items_available) mint indices // - for pNFT: // (u8) indicates whether to use a custom rule set // (Pubkey) custom rule set } /// --- programs/candy-machine-core/program/src/state/candy_machine_data.rs - /// Candy machine configuration data. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] pub struct CandyMachineData { /// Number of assets available pub items_available: u64, /// Symbol for the asset pub symbol: String, /// Secondary sales royalty basis points (0-10000) pub seller_fee_basis_points: u16, /// Max supply of each individual asset (default 0) pub max_supply: u64, /// Indicates if the asset is mutable or not (default yes) pub is_mutable: bool, /// List of creators pub creators: Vec<Creator>, /// Config line settings pub config_line_settings: Option<ConfigLineSettings>, /// Hidden setttings pub hidden_settings: Option<HiddenSettings>, } // Creator information. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct Creator { /// Pubkey address pub address: Pubkey, /// Whether the creator is verified or not pub verified: bool, // Share of secondary sales royalty pub percentage_share: u8, } /// Hidden settings for large mints used with off-chain data. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] pub struct HiddenSettings { /// Asset prefix name pub name: String, /// Shared URI pub uri: String, /// Hash of the hidden settings file pub hash: [u8; 32], } /// Config line settings to allocate space for individual name + URI. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] pub struct ConfigLineSettings { /// Common name prefix pub prefix_name: String, /// Length of the remaining part of the name pub name_length: u32, /// Common URI prefix pub prefix_uri: String, /// Length of the remaining part of the URI pub uri_length: u32, /// Indicates whether to use a senquential index generator or not pub is_sequential: bool, }

Initialize Candy Machine

initialize_v2 function from the Metaplex Candy Machine is a crucial part of setting up a new Candy Machine account. It initializes the state of candy machine, and gets verification authority from collection mint account.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Initialize the candy machine account with the specified data and token standard. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account (must be pre-allocated but zero content) /// 1. `[writable]` Authority PDA (seeds `["candy_machine", candy machine id]`) /// 2. `[]` Candy Machine authority /// 3. `[signer]` Payer /// 4. `[]` Collection metadata /// 5. `[]` Collection mint /// 6. `[]` Collection master edition /// 7. `[signer]` Collection update authority /// 8. `[writable]` Collection metadata delegate record /// 9. `[]` Token Metadata program /// 10. `[]` System program /// 11. `[]` Instructions sysvar account /// 12. `[optional]` Token Authorization Rules program /// 13. `[optional]` Token authorization rules account pub fn initialize_v2( ctx: Context<InitializeV2>, data: CandyMachineData, token_standard: u8, ) -> Result<()> { instructions::initialize_v2(ctx, data, token_standard) } /// --- programs/candy-machine-core/program/src/state/candy_machine_data.rs --- /// Candy machine configuration data. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Debug)] pub struct CandyMachineData { /// Number of assets available pub items_available: u64, /// Symbol for the asset pub symbol: String, /// Secondary sales royalty basis points (0-10000) pub seller_fee_basis_points: u16, /// Max supply of each individual asset (default 0) pub max_supply: u64, /// Indicates if the asset is mutable or not (default yes) pub is_mutable: bool, /// List of creators pub creators: Vec<Creator>, /// Config line settings pub config_line_settings: Option<ConfigLineSettings>, /// Hidden setttings pub hidden_settings: Option<HiddenSettings>, }
 
Steps:
  1. Token Standard Validation
    1. ensures that the token_standard provided is valid. The assert_token_standard function checks against the known values for token standards, ensuring that only supported standards are used.
  1. Required Space Calculation and Reallocation
      • Space Calculation: The get_space_for_candy method on the data struct calculates the required space for the Candy Machine's data. Space depends on candy machine’s setting. For config_line_settings setting, it needs space to store each NFT’s symbol and uri.
      • Reallocation for Rule Set: If the token standard is ProgrammableNonFungible, additional space is required for storing a rule set. If the current Candy Machine account does not have enough space, the resize_or_reallocate_account_raw function is called to allocate additional space. This step is crucial for ensuring that the account has enough storage for all the required data.
  1. Candy Machine Struct Initialization
      • Account Setup: The function begins setting up the CandyMachine struct, which contains all the relevant data for the Candy Machine.
      • Fields:
        • data: The configuration data passed as a parameter.
        • version: Sets the version to V2.
        • token_standard: Uses the provided token standard.
        • features: An array reserved for storing feature flags, initially set to zeros.
        • authority: The key of the authority account, which controls updates to the Candy Machine, like inserting config lines.
        • mint_authority: Initially set to the same authority, this can be changed later. This authority is allowed to mint from candy machine. Usually it is assigned to certain guard.
        • collection_mint: The mint account associated with the collection.
        • items_redeemed: Tracks how many NFTs have been redeemed; starts at zero.
  1. Symbol String Handling
    1. The fixed_length_string function pads the string as necessary ensuring that the symbol string for the Candy Machine adheres to a fixed length, determined by MAX_SYMBOL_LENGTH. This helps maintain a consistent storage layout of candy machine account faciliates data index.
  1. Data Validation
    1. Validates the hidden and config lines settings against the maximum allowed values for name and URI. Because hidden and config lines settings use string pattern to save space, so here is replaces the pattern with the maximum NFT index to get potential max string length and check the length.
  1. Storing Candy Machine Data
    1. The Candy Machine struct is serialized (converted into a byte vector) and then stored in the Candy Machine account's data. Note there is discriminator in the front which is a unique identifier for the Candy Machine account, ensuring that the account can be correctly identified as a Candy Machine.
  1. Initialize Config Setting Data
    1. If there are no hidden settings, which means the setting is config setting. Then the function sets the initial number of configuration lines to zero by writing a minimum value (u32::MIN 0) into the specified section of the account data. This value records how many lines are loaded. When candy machine authority inserts config lines, this value increments correspondingly.
  1. Storing Rule Set for Programmable Non-Fungible Tokens
    1. For Programmable Non-Fungible Tokens, the function stores the rule set public key in the Candy Machine account if a rule set is provided. The rule set helps define custom logic or constraints for the NFTs.
  1. Approving Metadata Delegate
    1. Delegate Approval: The function sets up the necessary accounts and approves a metadata delegate, allowing the Candy Machine to verify NFTs minted to the collection. This is crucial for ensuring that NFTs minted through candy machine can be verified.
/// --- programs/candy-machine-core/program/src/instructions/initialize_v2.rs --- pub fn initialize_v2( ctx: Context<InitializeV2>, data: CandyMachineData, token_standard: u8, ) -> Result<()> { /// Token Standard Validation // make sure we got a valid token standard assert_token_standard(token_standard)?; /// Required Space Calculation and Reallocation let required_length = data.get_space_for_candy()?; /// Candy Machine Struct Initialization if token_standard == TokenStandard::ProgrammableNonFungible as u8 && ctx.accounts.candy_machine.data_len() < (required_length + RULE_SET_LENGTH + 1) { msg!("Allocating space to store the rule set"); resize_or_reallocate_account_raw( &ctx.accounts.candy_machine.to_account_info(), &ctx.accounts.payer.to_account_info(), &ctx.accounts.system_program.to_account_info(), required_length + (1 + RULE_SET_LENGTH), )?; } let candy_machine_account = &mut ctx.accounts.candy_machine; let mut candy_machine = CandyMachine { data, version: AccountVersion::V2, token_standard, features: [0u8; 6], authority: ctx.accounts.authority.key(), mint_authority: ctx.accounts.authority.key(), collection_mint: ctx.accounts.collection_mint.key(), items_redeemed: 0, }; /// Symbol String Handling candy_machine.data.symbol = fixed_length_string(candy_machine.data.symbol, MAX_SYMBOL_LENGTH)?; /// Data Validation // validates the config lines settings candy_machine.data.validate()?; /// Storing Candy Machine Data let mut struct_data = CandyMachine::discriminator().try_to_vec().unwrap(); struct_data.append(&mut candy_machine.try_to_vec().unwrap()); let mut account_data = candy_machine_account.data.borrow_mut(); account_data[0..struct_data.len()].copy_from_slice(&struct_data); /// Initialize Config Setting Data if candy_machine.data.hidden_settings.is_none() { // set the initial number of config lines account_data[HIDDEN_SECTION..HIDDEN_SECTION + 4].copy_from_slice(&u32::MIN.to_le_bytes()); } /// Storing Rule Set for Programmable Non-Fungible Tokens if token_standard == TokenStandard::ProgrammableNonFungible as u8 { if let Some(rule_set_info) = &ctx.accounts.rule_set { msg!("Storing rule set pubkey"); let rule_set = rule_set_info.key(); account_data[required_length] = SET; let index = required_length + 1; let mut storage = &mut account_data[index..index + RULE_SET_LENGTH]; rule_set.serialize(&mut storage)?; } } /// Approving Metadata Delegate // approves the metadata delegate so the candy machine can verify minted NFTs let delegate_accounts = ApproveMetadataDelegateHelperAccounts { token_metadata_program: ctx.accounts.token_metadata_program.to_account_info(), authority_pda: ctx.accounts.authority_pda.to_account_info(), collection_metadata: ctx.accounts.collection_metadata.to_account_info(), collection_mint: ctx.accounts.collection_mint.to_account_info(), collection_update_authority: ctx.accounts.collection_update_authority.to_account_info(), delegate_record: ctx.accounts.collection_delegate_record.to_account_info(), payer: ctx.accounts.payer.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), sysvar_instructions: ctx.accounts.sysvar_instructions.to_account_info(), authorization_rules_program: ctx .accounts .authorization_rules_program .as_ref() .map(|authorization_rules_program| authorization_rules_program.to_account_info()), authorization_rules: ctx .accounts .authorization_rules .as_ref() .map(|authorization_rules| authorization_rules.to_account_info()), }; approve_metadata_delegate(delegate_accounts) }

Initialize Guard Account

initialize loads CandyGuardData from passed-in data, checks validity, and stores guard data.
 
Guard account basic state:
  1. base
    1. base account which deploys guard, participating guard account address calculation(needs to sign tx) which ensures guard account’s uniqueness.
  1. bump
    1. Bump seed. Store it to save computation unit.
  1. authority
    1. account has authority to attach this guard to any candy machine.
/// --- programs/candy-guard/program/src/lib.rs --- /// Create a new candy guard account. pub fn initialize(ctx: Context<Initialize>, data: Vec<u8>) -> Result<()> { instructions::initialize(ctx, data) } /// --- programs/candy-guard/program/src/instructions/initialize.rs --- pub fn initialize(ctx: Context<Initialize>, data: Vec<u8>) -> Result<()> { // deserializes the candy guard data let data = CandyGuardData::load(&data)?; // validates guard settings data.verify()?; let candy_guard = &mut ctx.accounts.candy_guard; candy_guard.base = ctx.accounts.base.key(); candy_guard.bump = *ctx.bumps.get("candy_guard").unwrap(); candy_guard.authority = ctx.accounts.authority.key(); let account_info = candy_guard.to_account_info(); let mut account_data = account_info.data.borrow_mut(); data.save(&mut account_data[DATA_OFFSET..]) } #[derive(Accounts)] #[instruction(data: Vec<u8>)] pub struct Initialize<'info> { #[account( init, payer = payer, space = DATA_OFFSET + data.len(), seeds = [SEED, base.key().as_ref()], bump )] pub candy_guard: Account<'info, CandyGuard>, // Base key of the candy guard PDA pub base: Signer<'info>, /// CHECK: authority can be any account and is not written to or read authority: UncheckedAccount<'info>, #[account(mut)] payer: Signer<'info>, pub system_program: Program<'info, System>, } /// --- programs/candy-guard/program/src/state/candy_guard.rs --- #[account] #[derive(Default)] pub struct CandyGuard { // Base key used to generate the PDA pub base: Pubkey, // Bump seed pub bump: u8, // Authority of the guard pub authority: Pubkey, // after this there is a flexible amount of data to serialize // data (CandyGuardData struct) of the available guards; the size // of the data is adjustable as new guards are implemented (the // account is resized using realloc) // // available guards: // 1) bot tax // 2) sol payment // 3) token payment // 4) start date // 5) third party signer // 6) token gate // 7) gatekeeper // 8) end date // 9) allow list // 10) mint limit // 11) nft payment // 12) redeemed amount // 13) address gate // 14) nft gate // 15) nft burn // 16) token burn // 17) freeze sol payment // 18) freeze token payment // 19) program gate // 20) allocation // 21) token2022 payment }

Wrap

wrap sets mint authority of candy machine to the created guard account. So that mint operation can only be performed through guard account.
Guard program calls candy machine program’s set_mint_authority instruction to set mint authority.
/// --- programs/candy-guard/program/src/lib.rs --- /// Add a candy guard to a candy machine. After the guard is added, mint /// is only allowed through the candy guard. pub fn wrap(ctx: Context<Wrap>) -> Result<()> { instructions::wrap(ctx) } /// --- programs/candy-guard/program/src/instructions/wrap.rs --- #[derive(Accounts)] pub struct Wrap<'info> { #[account(has_one = authority)] pub candy_guard: Account<'info, CandyGuard>, // candy guard authority pub authority: Signer<'info>, #[account( mut, constraint = candy_machine.authority == candy_machine_authority.key(), owner = mpl_candy_machine_core::id() )] pub candy_machine: Account<'info, CandyMachine>, /// CHECK: account constraints checked in account trait #[account(address = mpl_candy_machine_core::id())] pub candy_machine_program: AccountInfo<'info>, // candy machine authority pub candy_machine_authority: Signer<'info>, } /// --- programs/candy-guard/program/src/instructions/wrap.rs --- pub fn wrap(ctx: Context<Wrap>) -> Result<()> { let candy_guard = &ctx.accounts.candy_guard; // PDA signer for the transaction let seeds = [SEED, &candy_guard.base.to_bytes(), &[candy_guard.bump]]; let signer = [&seeds[..]]; let candy_machine_program = ctx.accounts.candy_machine_program.to_account_info(); let update_ix = SetMintAuthority { candy_machine: ctx.accounts.candy_machine.to_account_info(), authority: ctx.accounts.candy_machine_authority.to_account_info(), mint_authority: candy_guard.to_account_info(), }; let cpi_ctx = CpiContext::new_with_signer(candy_machine_program, update_ix, &signer); // candy machine set_mint_authority CPI set_mint_authority(cpi_ctx)?; Ok(()) }

Update Authorities

Using instruction set_authority, with old authority’s signature, we can change it to new authority.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Set a new authority of the candy machine. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account /// 1. `[signer]` Candy Machine authority pub fn set_authority(ctx: Context<SetAuthority>, new_authority: Pubkey) -> Result<()> { instructions::set_authority(ctx, new_authority) } /// --- programs/candy-machine-core/program/src/instructions/set_authority.rs --- pub fn set_authority(ctx: Context<SetAuthority>, new_authority: Pubkey) -> Result<()> { let candy_machine = &mut ctx.accounts.candy_machine; candy_machine.authority = new_authority; Ok(()) } /// Sets a new candy machine authority. #[derive(Accounts)] pub struct SetAuthority<'info> { /// Candy Machine account. #[account(mut, has_one = authority)] candy_machine: Account<'info, CandyMachine>, /// Autority of the candy machine. authority: Signer<'info>, }

Update Shared NFT Data

instruction update is used to update candy machine setting.
There are some limitations:
  1. can’t switch setting.
  1. when using config line setting, we can’t change available item count.
  1. if both are config line setting
    1. can’t change number of lines.
    2. can’t increase length of remaining name and uri
    3. can’t change is_sequential if some item has been redeemed.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Update the candy machine configuration. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account /// 1. `[signer]` Candy Machine authority pub fn update(ctx: Context<Update>, data: CandyMachineData) -> Result<()> { instructions::update(ctx, data) } /// --- programs/candy-machine-core/program/src/instructions/update.rs --- pub fn update(ctx: Context<Update>, data: CandyMachineData) -> Result<()> { let candy_machine = &mut ctx.accounts.candy_machine; if (data.items_available != candy_machine.data.items_available) && data.hidden_settings.is_none() { return err!(CandyError::CannotChangeNumberOfLines); } if candy_machine.data.items_available > 0 && candy_machine.data.hidden_settings.is_none() && data.hidden_settings.is_some() { return err!(CandyError::CannotSwitchToHiddenSettings); } let symbol = fixed_length_string(data.symbol.clone(), MAX_SYMBOL_LENGTH)?; // validates the config data settings data.validate()?; if let Some(config_lines) = &candy_machine.data.config_line_settings { if let Some(new_config_lines) = &data.config_line_settings { // it is only possible to update the config lines settings if the // new values are equal or smaller than the current ones if config_lines.name_length < new_config_lines.name_length || config_lines.uri_length < new_config_lines.uri_length { return err!(CandyError::CannotIncreaseLength); } if config_lines.is_sequential != new_config_lines.is_sequential && candy_machine.items_redeemed > 0 { return err!(CandyError::CannotChangeSequentialIndexGeneration); } } } else if data.config_line_settings.is_some() { return err!(CandyError::CannotSwitchFromHiddenSettings); } candy_machine.data = data; candy_machine.data.symbol = symbol; Ok(()) }

Update Collection

instruction set_collection_v2 updates collection. It calls metaplex revoke instruction to revoke candy machine’s delegate authority of the old collection, then it calls metaplex delegate instruction to get delegate authority of new collection.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Set the collection mint for the candy machine. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account (must be pre-allocated but zero content) /// 1. `[signer]` Candy Machine authority /// 2. `[]` Authority PDA (seeds `["candy_machine", candy machine id]`) /// 3. `[signer]` Payer /// 4. `[]` Collection update authority /// 5. `[]` Collection mint /// 6. `[]` Collection metadata /// 7. `[optional, writable]` Metadata delegate record /// 8. `[optional, writable]` Collection authority record /// 9. `[signer]` New collection update authority /// 10. `[]` New collection mint /// 11. `[]` New collection metadata /// 12. `[]` New collection master edition /// 13. `[writable]` New collection metadata delegate record /// 14. `[]` Token Metadata program /// 15. `[]` System program /// 16. `[]` Instructions sysvar account /// 17. `[optional]` Token Authorization Rules program /// 18. `[optional]` Token authorization rules account pub fn set_collection_v2(ctx: Context<SetCollectionV2>) -> Result<()> { instructions::set_collection_v2(ctx) } /// --- programs/candy-machine-core/program/src/instructions/set_collection.rs --- pub fn set_collection_v2(ctx: Context<SetCollectionV2>) -> Result<()> { let accounts = ctx.accounts; let candy_machine = &mut accounts.candy_machine; // check whether the new collection mint is the same as the current collection; when they // are the same, we are just using this instruction to update the collection delegate so // we don't enforce the "mint in progress" constraint if !cmp_pubkeys( accounts.new_collection_mint.key, &candy_machine.collection_mint, ) { if candy_machine.items_redeemed > 0 { return err!(CandyError::NoChangingCollectionDuringMint); } else if !cmp_pubkeys(accounts.collection_mint.key, &candy_machine.collection_mint) { return err!(CandyError::MintMismatch); } candy_machine.collection_mint = accounts.new_collection_mint.key(); } if matches!(candy_machine.version, AccountVersion::V2) { // revoking the existing metadata delegate let revoke_accounts = RevokeMetadataDelegateHelperAccounts { token_metadata_program: accounts.token_metadata_program.to_account_info(), authority_pda: accounts.authority_pda.to_account_info(), collection_metadata: accounts.collection_metadata.to_account_info(), collection_mint: accounts.collection_mint.to_account_info(), collection_update_authority: accounts.collection_update_authority.to_account_info(), delegate_record: accounts.collection_delegate_record.to_account_info(), payer: accounts.payer.to_account_info(), system_program: accounts.system_program.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), authorization_rules_program: None, authorization_rules: None, }; revoke_metadata_delegate( revoke_accounts, candy_machine.key(), *ctx.bumps.get("authority_pda").unwrap(), )?; } else { let collection_metadata_info = &accounts.collection_metadata; let collection_metadata: Metadata = Metadata::try_from(&collection_metadata_info.to_account_info())?; // revoking the existing collection authority let revoke_accounts = RevokeCollectionAuthorityHelperAccounts { authority_pda: accounts.authority_pda.to_account_info(), collection_authority_record: accounts.collection_delegate_record.to_account_info(), collection_metadata: accounts.collection_metadata.to_account_info(), collection_mint: accounts.collection_mint.to_account_info(), token_metadata_program: accounts.token_metadata_program.to_account_info(), }; revoke_collection_authority_helper( revoke_accounts, candy_machine.key(), *ctx.bumps.get("authority_pda").unwrap(), collection_metadata.token_standard, )?; // bump the version of the account since we are setting a metadata delegate candy_machine.version = AccountVersion::V2; } // approve a new metadata delegate let delegate_accounts = ApproveMetadataDelegateHelperAccounts { token_metadata_program: accounts.token_metadata_program.to_account_info(), authority_pda: accounts.authority_pda.to_account_info(), collection_metadata: accounts.new_collection_metadata.to_account_info(), collection_mint: accounts.new_collection_mint.to_account_info(), collection_update_authority: accounts.new_collection_update_authority.to_account_info(), delegate_record: accounts.new_collection_delegate_record.to_account_info(), payer: accounts.payer.to_account_info(), system_program: accounts.system_program.to_account_info(), sysvar_instructions: accounts.sysvar_instructions.to_account_info(), authorization_rules_program: accounts .authorization_rules_program .as_ref() .map(|authorization_rules_program| authorization_rules_program.to_account_info()), authorization_rules: accounts .authorization_rules .as_ref() .map(|authorization_rules| authorization_rules.to_account_info()), }; approve_metadata_delegate(delegate_accounts) }

Delete Candy Machines

Insert Items

add_config_lines inserts config lines into candy machine. It requires signature of cancy machine authority.
Parameters:
  1. index:
    1. the start position of insertion.
  1. config_lines
    1. names and urls of NFTs to be inserted.
Note that it’s fine to update same NFT’s data multiple times. There is a bitmap which trackes each NFT’s loading status. Only when loading unloaded NFT, will the count records totoal loaded NFTs increases.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Add the configuration (name + uri) of each NFT to the account data. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account /// 1. `[signer]` Candy Machine authority pub fn add_config_lines( ctx: Context<AddConfigLines>, index: u32, config_lines: Vec<ConfigLine>, ) -> Result<()> { instructions::add_config_lines(ctx, index, config_lines) } /// --- programs/candy-machine-core/program/src/instructions/add_config_lines.rs --- #[derive(Accounts)] pub struct AddConfigLines<'info> { /// Candy Machine account. #[account(mut, has_one = authority)] candy_machine: Account<'info, CandyMachine>, /// Autority of the candy machine. authority: Signer<'info>, } /// --- programs/candy-machine-core/program/src/instructions/add_config_lines.rs --- pub fn add_config_lines( ctx: Context<AddConfigLines>, index: u32, config_lines: Vec<ConfigLine>, ) -> Result<()> { let candy_machine = &mut ctx.accounts.candy_machine; let account_info = candy_machine.to_account_info(); // mutable reference to the account data (config lines are written in the // 'hidden' section of the data array) let mut data = account_info.data.borrow_mut(); // no risk overflow because you literally cannot store this many in an account // going beyond u32 only happens with the hidden settings candies let total = index .checked_add(config_lines.len() as u32) .ok_or(CandyError::NumericalOverflowError)?; if total > (candy_machine.data.items_available as u32) { return err!(CandyError::IndexGreaterThanLength); } else if config_lines.is_empty() { // there is nothing to do, so we can stop early msg!("Config lines array empty"); return Ok(()); } // hidden settings candies do not store config lines if candy_machine.data.hidden_settings.is_some() { return err!(CandyError::HiddenSettingsDoNotHaveConfigLines); } let config_line = if let Some(config_line) = &candy_machine.data.config_line_settings { config_line } else { return err!(CandyError::MissingConfigLinesSettings); }; let name_length = config_line.name_length as usize; let uri_length = config_line.uri_length as usize; let config_line_length = name_length + uri_length; // both name and uri can be empty when are using a replacement variable; there is // still a need to call the add_config_lines so their indices are written on the // account for the random index generation if config_line_length > 0 { let mut position = HIDDEN_SECTION + 4 + (index as usize) * config_line_length; for line in &config_lines { if name_length > 0 { let name = fixed_length_string(line.name.clone(), name_length)?; let name_bytes = name.as_bytes(); let name_slice: &mut [u8] = &mut data[position..position + name_length]; name_slice.copy_from_slice(name_bytes); position += name_length; } if uri_length > 0 { let uri = fixed_length_string(line.uri.clone(), uri_length)?; let uri_bytes = uri.as_bytes(); let uri_slice: &mut [u8] = &mut data[position..position + uri_length]; uri_slice.copy_from_slice(uri_bytes); position += uri_length; } } } // after adding the config lines, we need to update the mint indices - there are two arrays // controlling this process: (1) a bit-mask array to keep track which config lines are already // present on the data; (2) an array with mint indices, where indices are added when the config // line is added for the first time (when updating a config line, the index is not added again) // bit-mask let bit_mask_start = HIDDEN_SECTION + 4 + (candy_machine.data.items_available as usize) * config_line_length; // (unordered) indices for the mint let indices_start = bit_mask_start + (candy_machine .data .items_available .checked_div(8) .ok_or(CandyError::NumericalOverflowError)? + 1) as usize; // holds the total number of config lines let mut count = get_config_count(&data)?; for i in 0..config_lines.len() { let position = (index as usize) .checked_add(i) .ok_or(CandyError::NumericalOverflowError)?; let byte_position = bit_mask_start + position .checked_div(8) .ok_or(CandyError::NumericalOverflowError)?; // bit index corresponding to the position of the line let bit = 7 - position .checked_rem(8) .ok_or(CandyError::NumericalOverflowError)?; let mask = u8::pow(2, bit as u32); let current_value = data[byte_position]; data[byte_position] |= mask; msg!( "Config line processed: byte position={}, mask={}, current value={}, new value={}, bit position={}", byte_position - bit_mask_start, mask, current_value, data[byte_position], bit ); if current_value != data[byte_position] { // add the new index to the mint indices vec let index_position = indices_start + position * 4; data[index_position..index_position + 4] .copy_from_slice(&u32::to_le_bytes(position as u32)); count = count .checked_add(1) .ok_or(CandyError::NumericalOverflowError)?; msg!( "New config line added: position={}, total count={})", position, count, ); } } // updates the config lines count data[HIDDEN_SECTION..HIDDEN_SECTION + 4].copy_from_slice(&(count as u32).to_le_bytes()); Ok(()) }

Mint

mint_v2 instruction of candy machine core program handles NFT mint operation.
/// --- programs/candy-machine-core/program/src/lib.rs --- /// Mint an NFT. /// /// Only the candy machine mint authority is allowed to mint. This handler mints both /// NFTs and Programmable NFTs. /// /// # Accounts /// /// 0. `[writable]` Candy Machine account (must be pre-allocated but zero content) /// 1. `[writable]` Authority PDA (seeds `["candy_machine", candy machine id]`) /// 2. `[signer]` Candy Machine mint authority /// 3. `[signer]` Payer /// 4. `[writable]` Mint account of the NFT /// 5. `[]` Mint authority of the NFT /// 6. `[writable]` Metadata account of the NFT /// 7. `[writable]` Master edition account of the NFT /// 8. `[optional, writable]` Destination token account /// 9. `[optional, writable]` Token record /// 10. `[]` Collection delegate or authority record /// 11. `[]` Collection mint /// 12. `[writable]` Collection metadata /// 13. `[]` Collection master edition /// 14. `[]` Collection update authority /// 15. `[]` Token Metadata program /// 16. `[]` SPL Token program /// 17. `[optional]` SPL Associated Token program /// 18. `[]` System program /// 19. `[optional]` Instructions sysvar account /// 20. `[]` SlotHashes sysvar cluster data. pub fn mint_v2<'info>(ctx: Context<'_, '_, '_, 'info, MintV2<'info>>) -> Result<()> { instructions::mint_v2(ctx) }
 
mint_v2 function constructs related accounts, and calls process_mint to process mint.
/// --- programs/candy-machine-core/program/src/instructions/mint_v2.rs --- pub fn mint_v2<'info>(ctx: Context<'_, '_, '_, 'info, MintV2<'info>>) -> Result<()> { let accounts = MintAccounts { spl_ata_program: ctx .accounts .spl_ata_program .as_ref() .map(|spl_ata_program| spl_ata_program.to_account_info()), authority_pda: ctx.accounts.authority_pda.to_account_info(), collection_delegate_record: ctx.accounts.collection_delegate_record.to_account_info(), collection_master_edition: ctx.accounts.collection_master_edition.to_account_info(), collection_metadata: ctx.accounts.collection_metadata.to_account_info(), collection_mint: ctx.accounts.collection_mint.to_account_info(), collection_update_authority: ctx.accounts.collection_update_authority.to_account_info(), nft_owner: ctx.accounts.nft_owner.to_account_info(), nft_master_edition: ctx.accounts.nft_master_edition.to_account_info(), nft_metadata: ctx.accounts.nft_metadata.to_account_info(), nft_mint: ctx.accounts.nft_mint.to_account_info(), nft_mint_authority: ctx.accounts.nft_mint_authority.to_account_info(), payer: ctx.accounts.payer.to_account_info(), recent_slothashes: ctx.accounts.recent_slothashes.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), sysvar_instructions: Some(ctx.accounts.sysvar_instructions.to_account_info()), token: ctx .accounts .token .as_ref() .map(|token| token.to_account_info()), token_metadata_program: ctx.accounts.token_metadata_program.to_account_info(), spl_token_program: ctx.accounts.spl_token_program.to_account_info(), token_record: ctx .accounts .token_record .as_ref() .map(|token_record| token_record.to_account_info()), }; process_mint( &mut ctx.accounts.candy_machine, accounts, ctx.bumps["authority_pda"], ) }
 
process_mint
Steps:
  1. Check NFT metadata account is empty
  1. Check there is avaiable NFT to mint
  1. Check passed-in collection mint account
    1. ensure it matches collection mint of the candy machine.
  1. Check ownership of collection mint account
    1. ensure it belongs to metaplex program which means it follows metaplex program’s logic.
  1. Deserialize collection mint metadata
  1. Check update authority account of collection metadata
  1. Pick NFT to mint
      • for config line setting
          1. use recent slog hash and current timestamp to get random NFT to mint.
          1. It requires that all NFTs (defined by item_available) have been loaded.
          1. replace the index place holder with NFT index, then concat with NFT custom name and uri. Note that the custom name and uri is behind NFT index if NFT index exists.
      • for hidden setting
          1. replace the index place holder with NFT index.
  1. Update redeemed NFT count
  1. Construct creators array.
    1. Note one creator is set to be the candy machine pda itself which helps others identify which NFT is created by candy machine. And the royalty of candy machine pda is 0.
  1. Create NFT mint account and mint.
/// --- programs/candy-machine-core/program/src/instructions/mint_v2.rs --- /// Mint a new NFT. /// /// The index minted depends on the configuration of the candy machine: it could be /// a psuedo-randomly selected one or sequential. In both cases, after minted a /// specific index, the candy machine does not allow to mint the same index again. pub(crate) fn process_mint( candy_machine: &mut Box<Account<'_, CandyMachine>>, accounts: MintAccounts, bump: u8, ) -> Result<()> { /// Check NFT metadata account is empty if !accounts.nft_metadata.data_is_empty() { return err!(CandyError::MetadataAccountMustBeEmpty); } /// Check there is avaiable NFT to mint if candy_machine.items_redeemed >= candy_machine.data.items_available { return err!(CandyError::CandyMachineEmpty); } /// Check passed-in collection mint account if !cmp_pubkeys( &accounts.collection_mint.key(), &candy_machine.collection_mint, ) { return err!(CandyError::CollectionKeyMismatch); } /// Check ownership of collection mint account if !cmp_pubkeys(accounts.collection_metadata.owner, &mpl_token_metadata::ID) { return err!(CandyError::IncorrectOwner); } /// Deserialize collection mint metadata let collection_metadata_info = &accounts.collection_metadata; let collection_metadata: Metadata = Metadata::try_from(&collection_metadata_info.to_account_info())?; // Check update authority account of collection metadata if !cmp_pubkeys( &collection_metadata.update_authority, &accounts.collection_update_authority.key(), ) { return err!(CandyError::IncorrectCollectionAuthority); } /// Pick NFT to mint let recent_slothashes = &accounts.recent_slothashes; let data = recent_slothashes.data.borrow(); let most_recent = array_ref![data, 12, 8]; let clock = Clock::get()?; // seed for the random number is a combination of the slot_hash - timestamp let seed = u64::from_le_bytes(*most_recent).saturating_sub(clock.unix_timestamp as u64); let remainder: usize = seed .checked_rem(candy_machine.data.items_available - candy_machine.items_redeemed) .ok_or(CandyError::NumericalOverflowError)? as usize; let config_line = get_config_line(candy_machine, remainder, candy_machine.items_redeemed)?; /// Update redeemed NFT count candy_machine.items_redeemed = candy_machine .items_redeemed .checked_add(1) .ok_or(CandyError::NumericalOverflowError)?; // release the data borrow drop(data); /// Construct creators array. let mut creators: Vec<mpl_token_metadata::types::Creator> = vec![mpl_token_metadata::types::Creator { address: accounts.authority_pda.key(), verified: true, share: 0, }]; for c in &candy_machine.data.creators { creators.push(mpl_token_metadata::types::Creator { address: c.address, verified: false, share: c.percentage_share, }); } /// Create NFT mint account and mint. match candy_machine.version { AccountVersion::V1 => create( candy_machine, accounts, bump, config_line, creators, collection_metadata, ), AccountVersion::V2 => create_and_mint( candy_machine, accounts, bump, config_line, creators, collection_metadata, ), } }

Generate Random Number

Candy machine program uses mix of recent slot hash and timestamp to calculate random number, then calculate NFT index in remaining NFTs.
/// --- programs/candy-machine-core/program/src/instructions/mint_v2.rs --- pub(crate) fn process_mint( candy_machine: &mut Box<Account<'_, CandyMachine>>, accounts: MintAccounts, bump: u8, ) -> Result<()> { /// ... let recent_slothashes = &accounts.recent_slothashes; let data = recent_slothashes.data.borrow(); let most_recent = array_ref![data, 12, 8]; let clock = Clock::get()?; // seed for the random number is a combination of the slot_hash - timestamp let seed = u64::from_le_bytes(*most_recent).saturating_sub(clock.unix_timestamp as u64); let remainder: usize = seed .checked_rem(candy_machine.data.items_available - candy_machine.items_redeemed) .ok_or(CandyError::NumericalOverflowError)? as usize; /// ... }
 

Pick NFT

get_config_line handles picking NFT from remaining NFTs.
Parameter:
  • candy_machine: account of candy machine pda.
  • index: picked NFT’s index in remaining NFTs.
  • mint_number: total minted NFT count.
 
Hidden setting:
In hidden setting, all NFT shares same prefix, only differs in index. So it just calculates NFT name and url and return.
 
Config line setting:
It needs to get the indexth NFT’s specific name and url.
To perform this, candy machine program uses a clever method to store remaining NFT’s indexes. Each available NFT occupies 4 bytes space which records the index of this NFT in config lines data. When an NFT got minted, the index data of that NFT is changed to be index of the last available NFT. This ensures that all available NFT’s index data is in the front of the space which helps get next avaible NFT’s index.
Let me give an example, assume our candy machine has total 3 available NFT (candy_machine.items_available). Then index data will be like:
notion image
 
When user mints first NFT, if the random index is 1 which means the fetched NFT’s index is stored in the second entry. Then program fetches the second entry in the index space, and get the index of NFT in config lines. Finally, it fills the second entry with the last available NFT’s index. Now there are two available NFT left.
notion image
 
If another user mints with random index 0. Then index space will become:
notion image
/// --- programs/candy-machine-core/program/src/instructions/mint_v2.rs --- /// Selects and returns the information of a config line. /// /// The selection could be either sequential or random. pub fn get_config_line( candy_machine: &Account<'_, CandyMachine>, index: usize, mint_number: u64, ) -> Result<ConfigLine> { if let Some(hs) = &candy_machine.data.hidden_settings { return Ok(ConfigLine { name: replace_patterns(hs.name.clone(), mint_number as usize), uri: replace_patterns(hs.uri.clone(), mint_number as usize), }); } let settings = if let Some(settings) = &candy_machine.data.config_line_settings { settings } else { return err!(CandyError::MissingConfigLinesSettings); }; let account_info = candy_machine.to_account_info(); let mut account_data = account_info.data.borrow_mut(); // validates that all config lines were added to the candy machine let config_count = get_config_count(&account_data)? as u64; if config_count != candy_machine.data.items_available { return err!(CandyError::NotFullyLoaded); } // (1) determine the mint index (index is a random index on the available indices array) let value_to_use = if settings.is_sequential { mint_number as usize } else { let items_available = candy_machine.data.items_available; let indices_start = HIDDEN_SECTION + 4 + (items_available as usize) * candy_machine.data.get_config_line_size() + (items_available .checked_div(8) .ok_or(CandyError::NumericalOverflowError)? + 1) as usize; // calculates the mint index and retrieves the value at that position let mint_index = indices_start + index * 4; let value_to_use = u32::from_le_bytes(*array_ref![account_data, mint_index, 4]) as usize; // calculates the last available index and retrieves the value at that position let last_index = indices_start + ((items_available - mint_number - 1) * 4) as usize; let last_value = u32::from_le_bytes(*array_ref![account_data, last_index, 4]); // swap-remove: this guarantees that we remove the used mint index from the available array // in a constant time O(1) no matter how big the indices array is account_data[mint_index..mint_index + 4].copy_from_slice(&u32::to_le_bytes(last_value)); value_to_use }; // (2) retrieve the config line at the mint_index position let mut position = HIDDEN_SECTION + 4 + value_to_use * candy_machine.data.get_config_line_size(); let name_length = settings.name_length as usize; let uri_length = settings.uri_length as usize; let name = if name_length > 0 { let name_slice: &mut [u8] = &mut account_data[position..position + name_length]; let name = String::from_utf8(name_slice.to_vec()) .map_err(|_| CandyError::CouldNotRetrieveConfigLineData)?; name.trim_end_matches(NULL_STRING).to_string() } else { EMPTY_STR.to_string() }; position += name_length; let uri = if uri_length > 0 { let uri_slice: &mut [u8] = &mut account_data[position..position + uri_length]; let uri = String::from_utf8(uri_slice.to_vec()) .map_err(|_| CandyError::CouldNotRetrieveConfigLineData)?; uri.trim_end_matches(NULL_STRING).to_string() } else { EMPTY_STR.to_string() }; let complete_name = replace_patterns(settings.prefix_name.clone(), value_to_use) + &name; let complete_uri = replace_patterns(settings.prefix_uri.clone(), value_to_use) + &uri; Ok(ConfigLine { name: complete_name, uri: complete_uri, }) }
 

Create Metadata Account and mint NFT

Steps:
  1. Construct candy machine pda signer seeds
  1. Creates NFT mint and metadata account
    1. Call metaplex program Create instruction. Note here it sets candy machine pda account as update authority. This allows it to update nft metadata later, also it can set candy machine pda as a verified creator of the created NFT (In metadata initialization process, if the creator is update authority of mint and it is signer, it can be set as verified creator directly. It sets update authority to collection metadata’s update authority at the end.
  1. Mints NFT to defined owner
    1. Call metaplex program Mint instruction.
  1. Update NFT metadata
    1. Call metaplex program Update instruction
      1. updates nft metadata primary_sale_happened to be true
      1. update authorization rules of pNFT.
      1. updates nft metadata update authority to collection metadata’s update authority.
  1. Verify the minted nft into the collection
/// --- programs/candy-machine-core/program/src/instructions/mint_v2.rs --- /// Creates the metadata accounts and mint a new token. fn create_and_mint( candy_machine: &mut Box<Account<'_, CandyMachine>>, accounts: MintAccounts, bump: u8, config_line: ConfigLine, creators: Vec<mpl_token_metadata::types::Creator>, collection_metadata: Metadata, ) -> Result<()> { /// Construct candy machine pda signer seeds let candy_machine_key = candy_machine.key(); let authority_seeds = [ AUTHORITY_SEED.as_bytes(), candy_machine_key.as_ref(), &[bump], ]; let sysvar_instructions_info = accounts .sysvar_instructions .as_ref() .ok_or(CandyError::MissingInstructionsSysvar)?; /// Creates NFT mint and metadata account CreateV1CpiBuilder::new(&accounts.token_metadata_program) .metadata(&accounts.nft_metadata) .mint(&accounts.nft_mint, accounts.nft_mint.is_signer) .authority(&accounts.nft_mint_authority) .payer(&accounts.payer) .update_authority(&accounts.authority_pda, true) .master_edition(Some(&accounts.nft_master_edition)) .token_standard( if candy_machine.token_standard == TokenStandard::ProgrammableNonFungible as u8 { TokenStandard::ProgrammableNonFungible } else { TokenStandard::NonFungible }, ) .name(config_line.name) .uri(config_line.uri) .symbol(candy_machine.data.symbol.to_string()) .seller_fee_basis_points(candy_machine.data.seller_fee_basis_points) .is_mutable(candy_machine.data.is_mutable) .creators(creators) .collection(Collection { verified: false, key: candy_machine.collection_mint, }) .decimals(0) .print_supply(if candy_machine.data.max_supply == 0 { PrintSupply::Zero } else { PrintSupply::Limited(candy_machine.data.max_supply) }) .system_program(&accounts.system_program) .sysvar_instructions(sysvar_instructions_info) .spl_token_program(&accounts.spl_token_program) .invoke_signed(&[&authority_seeds])?; /// Mints NFT to defined owner let token_info = accounts .token .as_ref() .ok_or(CandyError::MissingTokenAccount)?; let token_record_info = if candy_machine.token_standard == TokenStandard::ProgrammableNonFungible as u8 { Some( accounts .token_record .as_ref() .ok_or(CandyError::MissingTokenRecord)?, ) } else { None }; let spl_ata_program_info = accounts .spl_ata_program .as_ref() .ok_or(CandyError::MissingSplAtaProgram)?; MintV1CpiBuilder::new(&accounts.token_metadata_program) .token(token_info) // if we are initializing a new token account, we need to pass the // token owner; otherwise, we pass `None` .token_owner(if token_info.data_is_empty() { Some(&accounts.nft_owner) } else { None }) .metadata(&accounts.nft_metadata) .master_edition(Some(&accounts.nft_master_edition)) .mint(&accounts.nft_mint) .payer(&accounts.payer) .authority(&accounts.authority_pda) .token_record(token_record_info) .system_program(&accounts.system_program) .sysvar_instructions(sysvar_instructions_info) .spl_token_program(&accounts.spl_token_program) .spl_ata_program(spl_ata_program_info) .amount(1) .invoke_signed(&[&authority_seeds])?; // changes the update authority, primary sale happened, authorization rules let mut update_cpi = UpdateV1CpiBuilder::new(&accounts.token_metadata_program); update_cpi .authority(&accounts.authority_pda) .token(Some(token_info)) .metadata(&accounts.nft_metadata) .edition(Some(&accounts.nft_master_edition)) .mint(&accounts.nft_mint) .payer(&accounts.payer) .system_program(&accounts.system_program) .sysvar_instructions(sysvar_instructions_info) .primary_sale_happened(true) .new_update_authority(collection_metadata.update_authority); if candy_machine.token_standard == TokenStandard::ProgrammableNonFungible as u8 { let candy_machine_info = candy_machine.to_account_info(); let account_data = candy_machine_info.data.borrow_mut(); // the rule set for a newly minted pNFT is determined by: // 1. check if there is a rule set stored on the account; otherwise // 2. use the rule set from the collection metadata let candy_machine_rule_set = candy_machine.get_rule_set(&account_data, &collection_metadata)?; if let Some(rule_set) = candy_machine_rule_set { update_cpi.rule_set(RuleSetToggle::Set(rule_set)); } } update_cpi.invoke_signed(&[&authority_seeds])?; /// Verify the minted nft into the collection VerifyCollectionV1CpiBuilder::new(&accounts.token_metadata_program) .authority(&accounts.authority_pda) .delegate_record(Some(&accounts.collection_delegate_record)) .metadata(&accounts.nft_metadata) .collection_mint(&accounts.collection_mint) .collection_metadata(Some(&accounts.collection_metadata)) .collection_master_edition(Some(&accounts.collection_master_edition)) .system_program(&accounts.system_program) .sysvar_instructions(sysvar_instructions_info) .invoke_signed(&[&authority_seeds]) .map_err(|error| error.into()) }

Mint Through Guard

Guard program’s mint_v2 instruction handles mint operation. It loads guard, check whether all conditions satisifies, executes operations required by guards, like sol payment, etc. Then it calls candy machine program to mint NFT.
/// --- programs/candy-guard/program/src/lib.rs --- /// Mint an NFT from a candy machine wrapped in the candy guard. pub fn mint_v2<'info>( ctx: Context<'_, '_, '_, 'info, MintV2<'info>>, mint_args: Vec<u8>, label: Option<String>, ) -> Result<()> { instructions::mint_v2(ctx, mint_args, label) } /// --- programs/candy-guard/program/src/instructions/mint_v2.rs --- pub fn mint_v2<'info>( ctx: Context<'_, '_, '_, 'info, MintV2<'info>>, mint_args: Vec<u8>, label: Option<String>, ) -> Result<()> { let accounts = MintAccounts { candy_guard: &ctx.accounts.candy_guard, candy_machine: &ctx.accounts.candy_machine, candy_machine_authority_pda: ctx.accounts.candy_machine_authority_pda.to_account_info(), _candy_machine_program: ctx.accounts.candy_machine_program.to_account_info(), collection_delegate_record: ctx.accounts.collection_delegate_record.to_account_info(), collection_master_edition: ctx.accounts.collection_master_edition.to_account_info(), collection_metadata: ctx.accounts.collection_metadata.to_account_info(), collection_mint: ctx.accounts.collection_mint.to_account_info(), collection_update_authority: ctx.accounts.collection_update_authority.to_account_info(), nft_master_edition: ctx.accounts.nft_master_edition.to_account_info(), nft_metadata: ctx.accounts.nft_metadata.to_account_info(), nft_mint: ctx.accounts.nft_mint.to_account_info(), nft_mint_authority: ctx.accounts.nft_mint_authority.to_account_info(), payer: ctx.accounts.payer.to_account_info(), minter: ctx.accounts.minter.to_account_info(), recent_slothashes: ctx.accounts.recent_slothashes.to_account_info(), spl_ata_program: ctx .accounts .spl_ata_program .as_ref() .map(|spl_ata_program| spl_ata_program.to_account_info()), spl_token_program: ctx.accounts.spl_token_program.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), sysvar_instructions: ctx.accounts.sysvar_instructions.to_account_info(), token: ctx .accounts .token .as_ref() .map(|token| token.to_account_info()), token_metadata_program: ctx.accounts.token_metadata_program.to_account_info(), token_record: ctx .accounts .token_record .as_ref() .map(|token_record| token_record.to_account_info()), remaining: ctx.remaining_accounts, authorization_rules_program: ctx .accounts .authorization_rules_program .as_ref() .map(|authorization_rules_program| authorization_rules_program.to_account_info()), authorization_rules: ctx .accounts .authorization_rules .as_ref() .map(|authorization_rules| authorization_rules.to_account_info()), }; // evaluation context for this transaction let mut ctx = EvaluationContext { accounts, account_cursor: 0, args_cursor: 0, indices: BTreeMap::new(), }; process_mint(&mut ctx, mint_args, label) }
 
process_mint function manages the minting of NFTs while ensuring that all necessary validations are performed, both in terms of the transaction data and the enabled "guards" (specific conditions or restrictions applied to minting).
Steps:
  1. Load the Active Guard Set:
      • CandyGuardData::active_set: This function retrieves the "active" guard set for a specific minting operation, based on the provided label. If the program fails to load the active guard set (perhaps due to an incorrect label or an issue in the instruction), it defaults to loading the basic guard set.
      • This ensures that even in case of errors in loading guard set groups, the default guard set can still apply relevant rules such as the "bot tax."
  1. Retrieve Enabled Guard Conditions:
    1. retrieves a list of "enabled" guard conditions that must be met for the mint to proceed. These guards can impose various restrictions, such as limiting minting to specific wallets, time-based constraints, or payment requirements.
  1. Validate Transaction Data:
      • Before processing any mint actions, the transaction is validated. The validate function ensures that the mint transaction's critical components, such as collection mint keys and ownership of metadata, are correct.
      • If validation fails, it triggers the process_error function, potentially applying a bot tax if one is defined in the guard set.
  1. Validating Guards:
    1. Each guard condition is validated. If any guard condition is violated (e.g., trying to mint outside of allowed times), the process_error function is triggered. This could apply a bot tax to penalize unauthorized transactions or halt the minting process.
  1. Pre-Actions for Conditions:
    1. The pre_actions method for each guard condition is called, which can execute any actions required before the minting itself, such as reserving resources, performing necessary state updates, or ensuring other prerequisites are satisfied.
  1. Minting the NFT:
      • This is where the core action of minting the NFT occurs. The function cpi_mint (Cross-Program Invocation) calls the Candy Machine program's minting instruction, which actually mints the NFT and updates the relevant accounts (payer, minter, etc.).
      • This action is the culmination of all validations and preconditions.
  1. Post-Actions for Conditions:
    1. After minting, the post_actions method for each guard condition is executed. These actions might involve updating the state to reflect the mint, tracking usage of mint allowances, or finalizing any conditions related to the guard set.
/// --- programs/candy-guard/program/src/instructions/mint_v2.rs --- pub fn process_mint( ctx: &mut EvaluationContext<'_, '_, '_>, mint_args: Vec<u8>, label: Option<String>, ) -> Result<()> { let account_info = ctx.accounts.candy_guard.to_account_info(); let account_data = account_info.data.borrow(); /// Load the Active Guard Set: let guard_set = match CandyGuardData::active_set(&account_data[DATA_OFFSET..], label) { Ok(guard_set) => guard_set, Err(error) => { // load the default guard set to look for the bot_tax since errors only occur // when trying to load guard set groups let guard_set = CandyGuardData::load(&account_data[DATA_OFFSET..])?; return process_error(ctx, &guard_set.default, error); } }; /// Retrieve Enabled Guard Conditions: let conditions = guard_set.enabled_conditions(); /// Validate Transaction Data: if let Err(error) = validate(ctx) { return process_error(ctx, &guard_set, error); } // validates enabled guards (any error at this point is subject to bot tax) for condition in &conditions { if let Err(error) = condition.validate(ctx, &guard_set, &mint_args) { return process_error(ctx, &guard_set, error); } } // after this point, errors might occur, which will cause the transaction to fail // no bot tax from this point since the actions must be reverted in case of an error /// Pre-Actions for Conditions: for condition in &conditions { condition.pre_actions(ctx, &guard_set, &mint_args)?; } /// Minting the NFT: cpi_mint(ctx)?; /// Post-Actions for Conditions: for condition in &conditions { condition.post_actions(ctx, &guard_set, &mint_args)?; } Ok(()) }
 
Each guard condition implements trait Condition. Function in trait Condition will be called during mint to ensure the guard condition is met.
Below is example of guard ThirdPartySigner. We can see that it requires signature of required account.
/// --- programs/candy-guard/program/src/guards/third_party_signer.rs --- /// Guard that requires a specified signer to validate the transaction. /// /// List of accounts required: /// /// 0. `[signer]` Signer of the transaction. #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct ThirdPartySigner { pub signer_key: Pubkey, } impl Condition for ThirdPartySigner { fn validate<'info>( &self, ctx: &mut EvaluationContext, _guard_set: &GuardSet, _mint_args: &[u8], ) -> Result<()> { let signer_index = ctx.account_cursor; ctx.account_cursor += 1; let signer_account = try_get_account_info(ctx.accounts.remaining, signer_index)?; if !(cmp_pubkeys(signer_account.key, &self.signer_key) && signer_account.is_signer) { return err!(CandyGuardError::MissingRequiredSignature); } Ok(()) } } /// --- programs/candy-guard/program/src/guards/mod.rs --- pub trait Condition { /// Validate the condition of the guard. When the guard condition is /// not satisfied, it will return an error. /// /// This function should not perform any modification to accounts, since /// other guards might fail, causing the transaction to be aborted. /// /// Intermediary evaluation data can be stored in the `evaluation_context`, /// which will be shared with other guards and reused in the `actions` step /// of the process. fn validate( &self, ctx: &mut EvaluationContext, guard_set: &GuardSet, mint_args: &[u8], ) -> Result<()>; /// Perform the action associated with the guard before the CPI `mint` instruction. /// /// This function only gets called when all guards have been successfuly validated. /// Any error generated will make the transaction to fail. fn pre_actions( &self, _ctx: &mut EvaluationContext, _guard_set: &GuardSet, _mint_args: &[u8], ) -> Result<()> { Ok(()) } /// Perform the action associated with the guard after the CPI `mint` instruction. /// /// This function only gets called when all guards have been successfuly validated. /// Any error generated will make the transaction to fail. fn post_actions( &self, _ctx: &mut EvaluationContext, _guard_set: &GuardSet, _mint_args: &[u8], ) -> Result<()> { Ok(()) } }
 
cpi_mint calls candy machine program’s MintV2 function.
/// --- programs/candy-guard/program/src/instructions/mint_v2.rs --- /// Send a mint transaction to the candy machine. fn cpi_mint(ctx: &EvaluationContext) -> Result<()> { let candy_guard = &ctx.accounts.candy_guard; // candy machine mint instruction accounts let mint_accounts = Box::new(mpl_candy_machine_core::cpi::accounts::MintV2 { candy_machine: ctx.accounts.candy_machine.to_account_info(), authority_pda: ctx.accounts.candy_machine_authority_pda.clone(), mint_authority: candy_guard.to_account_info(), payer: ctx.accounts.payer.clone(), nft_owner: ctx.accounts.minter.clone(), nft_mint: ctx.accounts.nft_mint.clone(), nft_mint_authority: ctx.accounts.nft_mint_authority.clone(), nft_metadata: ctx.accounts.nft_metadata.clone(), nft_master_edition: ctx.accounts.nft_master_edition.clone(), token: ctx.accounts.token.clone(), token_record: ctx.accounts.token_record.clone(), collection_delegate_record: ctx.accounts.collection_delegate_record.clone(), collection_mint: ctx.accounts.collection_mint.clone(), collection_metadata: ctx.accounts.collection_metadata.clone(), collection_master_edition: ctx.accounts.collection_master_edition.clone(), collection_update_authority: ctx.accounts.collection_update_authority.clone(), token_metadata_program: ctx.accounts.token_metadata_program.clone(), spl_token_program: ctx.accounts.spl_token_program.clone(), spl_ata_program: ctx.accounts.spl_ata_program.clone(), system_program: ctx.accounts.system_program.clone(), sysvar_instructions: ctx.accounts.sysvar_instructions.clone(), recent_slothashes: ctx.accounts.recent_slothashes.clone(), authorization_rules_program: ctx.accounts.authorization_rules_program.clone(), authorization_rules: ctx.accounts.authorization_rules.clone(), }); let mint_infos = mint_accounts.to_account_infos(); let mut mint_metas = mint_accounts.to_account_metas(None); mint_metas.iter_mut().for_each(|account_meta| { if account_meta.pubkey == ctx.accounts.nft_mint.key() { account_meta.is_signer = ctx.accounts.nft_mint.is_signer; } }); let mint_ix = Instruction { program_id: mpl_candy_machine_core::ID, accounts: mint_metas, data: mpl_candy_machine_core::instruction::MintV2::DISCRIMINATOR.to_vec(), }; // PDA signer for the transaction let seeds = [SEED, &candy_guard.base.to_bytes(), &[candy_guard.bump]]; let signer = [&seeds[..]]; invoke_signed(&mint_ix, &mint_infos, &signer)?; Ok(()) }

Reference

  1. https://developers.metaplex.com/candy-machine