learn.sol
Week 3 • Token Minting Project

Project: Token Minting & Supply Control

Build an Anchor token project with PDA mint authority, controlled minting, and test-driven supply rules suitable for beginner-to-intermediate learners.

This project turns the previous lesson into a real mini-system.

You will create a token where your program controls minting rules, not the front end.

Project Goal

Build a reward token with these constraints:

  • Mint authority is a PDA
  • Only approved admin can trigger mint
  • Total minted supply cannot exceed MAX_SUPPLY

Architecture

  • Config PDA stores admin and minted_total
  • Mint authority PDA signs CPI via seeds
  • Instruction checks cap before minting

Program Skeleton

#[account]
#[derive(InitSpace)]
pub struct Config {
    pub admin: Pubkey,
    pub minted_total: u64,
}

pub const MAX_SUPPLY: u64 = 1_000_000_000;

Initialize Config + Mint Authority

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub admin: Signer<'info>,

    #[account(
        init,
        payer = admin,
        space = 8 + Config::INIT_SPACE,
        seeds = [b"config"],
        bump,
    )]
    pub config: Account<'info, Config>,

    /// CHECK: PDA used as mint authority signer
    #[account(seeds = [b"mint-authority"], bump)]
    pub mint_authority: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

Controlled Mint Instruction

pub fn mint_with_cap(ctx: Context<MintWithCap>, amount: u64) -> Result<()> {
    require_keys_eq!(ctx.accounts.admin.key(), ctx.accounts.config.admin, CustomError::Unauthorized);

    let next_total = ctx.accounts.config
        .minted_total
        .checked_add(amount)
        .ok_or(CustomError::Overflow)?;

    require!(next_total <= MAX_SUPPLY, CustomError::SupplyCapExceeded);

    let signer_seeds: &[&[&[u8]]] = &[&[b"mint-authority", &[ctx.bumps.mint_authority]]];

    let cpi_accounts = anchor_spl::token_interface::MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.destination.to_account_info(),
        authority: ctx.accounts.mint_authority.to_account_info(),
    };

    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts,
        signer_seeds,
    );

    anchor_spl::token_interface::mint_to(cpi_ctx, amount)?;

    ctx.accounts.config.minted_total = next_total;
    Ok(())
}

What Makes This Intermediate

  • PDA signer flow with new_with_signer
  • State + token CPI consistency
  • Supply invariants enforced on-chain

Test Plan

  1. Initialize config sets admin correctly
  2. Authorized mint updates minted_total
  3. Unauthorized mint fails
  4. Mint over cap fails

Do not enforce supply cap only in the client. Always enforce it in program logic.

Shipping Checklist

Step 1: Program tests pass

Your failure-path tests should be as strong as success tests.

Step 2: IDL regenerated

Rebuild to keep client instruction names and account schemas aligned.

Step 3: Client uses explicit cluster config

No hidden RPC defaults in scripts or UI.

Try This Next

  1. Add per-user mint quotas using a UserQuota PDA.
  2. Add pause/unpause admin switch to config.
  3. Add event emission for each mint and index it off-chain.

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Project: Token Minting & Supply Control | learn.sol