Working with Tokens on Solana using Native Rust

Tokens represent money-like assets, utility (governance or access), rewards and points, game items, tickets, and more. Most tokens on Solana follow the SPL Token standard (or Token-2022 with extensions), which defines how tokens are created, transferre…


This content originally appeared on DEV Community and was authored by David Ekete

Tokens represent money-like assets, utility (governance or access), rewards and points, game items, tickets, and more. Most tokens on Solana follow the SPL Token standard (or Token-2022 with extensions), which defines how tokens are created, transferred, and burned.

This article will cover how tokens work on Solana and how you can use them in your program. The examples in this article are in Native Rust, but the concepts can be applied to any framework/library (Anchor, Pinocchio, Steel).

How Tokens Work on Solana

Every token on Solana has a mint account. A mint account defines key information about the token, such as the decimals, authority (who can mint and optionally freeze), total supply, etc. However, the mint account only defines the token; it doesn’t store it.

Token decimals describe the asset’s fractional precision. If a mint has d decimals, then 1 token = 10^d base units. Because on-chain programs use integers only to keep consensus deterministic (floating-point can round differently across machines), wallets and dapps convert to/from base units by multiplying or dividing by 10^d. For example, many fiat-like tokens (e.g., USDC) use 6 decimals, so 1 USDC = 1,000,000 base units.

Tokens can exist as fungible or non-fungible (NFTs). Fungible tokens typically use decimals like 6 or 9 for divisibility, while NFTs use 0 decimals and a supply of 1 (or small editions).

The token balances live in Associated Token Accounts (ATAs). ATAs are deterministically derived from an (owner, mint, token_program_id) tuple. Names, symbols, and logos aren’t in the mint or token account. UIs read them from a Metaplex Token Metadata account linked to the mint.

How tokens work on Solana

Most resources on the web currently cover creating tokens on the client side with a CLI or SDK because it’s simpler, cheaper (no CPI), and fits one-off user-issued tokens or NFTs.

This guide takes the other path, creating mints on-chain inside your program using PDAs and program-controlled authorities. That approach is crucial when the token is core to protocol safety or pricing, e.g., AMM LP shares, vault/receipt tokens, etc., and as such, the examples in this tutorial are helper functions that can be part of a bigger program, not a standalone one.

Setting Up Your Development Environment

To follow along with this tutorial, you will need to set up a minimal Native Rust program.

Start by running the command below to create a new project:

cargo init token-mints --lib

Then add the following crates by running the command below:

cargo add borsh@1.5.7 && \
cargo add solana-program@2.3.0 && \
cargo add solana-system-interface@1.0.0 --features bincode && \
cargo add spl-associated-token-account@7.0.0 --features no-entrypoint && \
cargo add spl-token@7.0.0 --features no-entrypoint

The crates you added above include:

  • borsh – Deterministic binary serialization/deserialization for your program/account data. Commonly used to (de)serialize instruction payloads and account state.
  • solana-program: types like Pubkey, AccountInfo, ProgramResult, entrypoint glue, sysvars, and helpers to build/parse instructions.
  • solana-system-interface (+ bincode feature) – Stable interface types/IDL for the System Program (create accounts, transfers, etc.).
  • spl-associated-token-account (no-entrypoint) – Instruction builders and CPI interface for the Associated Token Account (ATA) Program (create/find ATAs).
  • spl-token (no-entrypoint) – Instruction builders, state types, and CPI interface for the SPL Token Program (mint, transfer, burn, etc.).

Note: Ensure you install the exact versions in the command above, else this may not work as expected.

Creating and Initializing a Token Mint

In this section, you’ll build a small helper, create_and_init_mint, that allocates a mint account as a PDA, then initializes it so your program (via a PDA) controls minting. We’ll set the freeze authority to None to keep the asset non-custodial by default.

The function will accept the following inputs:

  • Accounts:
    • Payer (signer, writable): funds rent and signs the create account instruction.
    • Mint (PDA, writable, uninitialized): the account to become your mint.
    • System Program*:* required by the system create_account.
  • program_id: your program ID.
  • token_decimals: u8
  • mint_authority: Pubkey (recommend a program PDA)
  • mint_seeds: &[&[u8]] (the PDA seeds/bump for the mint)

Before you proceed, ensure that you have the following imports at the top of your file:

use solana_program::account_info::{next_account_info, AccountInfo};
use solana_program::entrypoint::ProgramResult;
use solana_program::program::{invoke, invoke_signed};
use solana_program::program_error::ProgramError;
use solana_program::program_pack::Pack;
use solana_program::pubkey::Pubkey;
use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;

use spl_token::id as spl_token_id;
use spl_token::instruction as token_instruction;
use spl_token::state::{Account as SplAccount, Mint as SplMint};

Note that the imports from spl_token are renamed to avoid conflicts with packages from solana_program.

Creating the Token Mint

To create the mint, first, you allocate the mint account as a program-derived address (PDA) and set its owner to the SPL Token program. This ensures that, after creation, only the token program itself can mutate the mint’s data.

Next, you set the mint authority to a PDA controlled by your program, so any future issuance must come through your on-chain logic (you’ll sign CPIs with invoke_signed).

You then intentionally leave the freeze authority as None, avoiding a built-in backdoor that could freeze user accounts. Finally, you choose the decimals explicitly to set the asset’s precision up front (e.g., 6 for fiat-like tokens, 9 for Solana-style precision, 0 for NFTs).

Like so:

pub fn create_and_init_mint(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    mint_authority: &Pubkey,
    mint_seeds: &[&[u8]],
    token_decimals: u8,
) -> ProgramResult {
    let acc_iter = &mut accounts.iter();

    //payer (signer), mint (writable), system program
    let payer = next_account_info(acc_iter)?;
    let token_mint = next_account_info(acc_iter)?;
    let system_program = next_account_info(acc_iter)?;

    if !payer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    if !token_mint.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    // Ensure the passed mint is exactly the PDA we expect for these seeds.
    let expected = Pubkey::create_program_address(mint_seeds, program_id)
        .map_err(|_| ProgramError::InvalidSeeds)?;
    if *token_mint.key != expected {
        return Err(ProgramError::InvalidSeeds);
    }

    let space = SplMint::LEN as u64;
    let lamports = Rent::get()?.minimum_balance(space as usize);

    invoke_signed(
        &solana_system_interface::instruction::create_account(
            &payer.key,
            &token_mint.key,
            lamports,
            space,
            &spl_token_id(),
        ),
        &[payer.clone(), token_mint.clone(), system_program.clone()],
        &[mint_seeds],
    )?;

Now, you have created the token mint; next, you will initialize it.

Initializing Token Mints

To initialize the token mint, you need to invoke the initialize_mint2 instruction from the SPL token program. Pass the SPL program ID, the mint account’s public key, the mint_authority, the freeze authority, and the decimals into the instructions and invoke the instruction with the token mint account.

Like so (this snippet is part of create_and_init_mint):

    let initialize_ix = token_instruction::initialize_mint2(
        &spl_token_id(),
        token_mint.key,
        mint_authority,
        None,
        token_decimals,
    )?;

    invoke(&initialize_ix, &[token_mint.clone()])?;

    Ok(())

  }

There’s an alternative to the initialize_mint2 instruction, which is the initialize_mint function. Both set the same mint fields (decimals, mint authority, freeze authority). The difference is in the required accounts:

  • initialize_mint2 does not require the Rent sysvar; you pass only the mint account in the CPI account list.
  • initialize_mint requires the Rent sysvar to be passed and will read it during initialization.

After initialization, the mint’s total supply is 0. The SPL Token program does not enforce a cap; your supply policy is up to you.

You create supply by calling mint_to_checked from your mint authority (usually a PDA) into a token account (often a treasury ATA). However, if you want a fixed supply, mint the full amount and then revoke the mint authority by setting it to None (This process is irreversible).

Here’s an example:

// Mint initial supply to a treasury ATA, then lock the mint.
let amount_base = ui_amount
    .checked_mul(10u64.pow(token_decimals as u32))
    .ok_or(ProgramError::InvalidArgument)?;

let ix = token_instruction::mint_to_checked(
    &spl_token_id(),
    token_mint.key,
    treasury_ata.key,
    mint_authority,      // your PDA signs via invoke_signed
    &[],
    amount_base,
    token_decimals,
)?;
invoke(&ix, &[token_mint.clone(), treasury_ata.clone(), /* + authority account */])?;

// Later, make supply fixed:
let ix_disable = token_instruction::set_authority(
    &spl_token_id(),
    token_mint.key,
    None, // new authority = None → revoke
    spl_token::instruction::AuthorityType::MintTokens,
    mint_authority,
    &[],
)?;
invoke(&ix_disable, &[token_mint.clone(), /* + authority account */])?;

Now, you have created and initialized the token mint. Next, you will create associated token accounts for the mints.

Creating an Associated Token Account (ATA)

For any user to hold the token you just minted and initialized, they have to own an ATA for the mint. An ATA is a deterministic address derived from an (owner, mint, token_program_id) tuple, so wallets and programs can compute it and fetch that single account.

When creating ATAs, decide who pays the rent to initialize the account. You can sponsor creation from your program (great onboarding, but costs scale with users), or require the user to fund it themselves (adds a signature step and a bit of friction, but controls your spend). Anyone may pay the fee; payer and owner do not have to be the same, and the payer gains no control over the account.

To create an ATA, invoke the create_associated_token_account_idempotent instruction from the spl_associated_token_account::instruction crate with the following accounts (in the same order): payer, owner, ATA account, token mint account, token program account, system program account.

Like so:

use spl_associated_token_account as ata;

pub fn create_ata_for(accounts: &[AccountInfo]) -> ProgramResult {
    let acc_iter = &mut accounts.iter();

    let payer = next_account_info(acc_iter)?; //pays the fees for account creation
    let owner = next_account_info(acc_iter)?; //Owner of the ATA
    let ata_acc = next_account_info(acc_iter)?;
    let token_mint = next_account_info(acc_iter)?;
    let token_program = next_account_info(acc_iter)?;
    let system_program = next_account_info(acc_iter)?;

    // Derive the expected ATA address and compare
    let expected_ata = ata::get_associated_token_address_with_program_id(
        owner.key,
        token_mint.key,
        &spl_token_id(),
    );

    // Sanity check: is the passed ATA the one we expect?
    if ata_acc.key != &expected_ata {
        return Err(ProgramError::InvalidArgument);
    }

    // Payer must sign
    if !payer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Create the ATA (idempotent)
    let ata_instruction = ata::instruction::create_associated_token_account_idempotent(
        payer.key,
        owner.key,
        token_mint.key,
        &spl_token_id(),
    );

    // Invoke the ATA creation instruction
    invoke(
        &ata_instruction,
        &[
            payer.clone(),
            ata_acc.clone(),
            owner.clone(),
            token_mint.clone(),
            system_program.clone(),
            token_program.clone(),
        ],
    )?;

    Ok(())
}

Now you can create ATAs for your mints. Next, you will learn how to burn tokens from the ATAs.

Burning Tokens

Burning destroys tokens from a specific token account and reduces the mint’s total supply by the same amount. The SPL Token program enforces that only the token account owner (or an approved delegate) may burn from that account; you do not need the mint authority.

To burn tokens, invoke the spl token programs burn_checked instruction with the following accounts (in the same order): token mint account, ATA owner account, ATA.

Like so:

pub fn burn_user_tokens(accounts: &[AccountInfo], amount_ui: u64) -> ProgramResult {
    let acc_iter = &mut accounts.iter();

    // 0 mint, 1 owner(signer), 2 token_account(ATA), 3 token_program
    let mint_account = next_account_info(acc_iter)?;
    let owner_account = next_account_info(acc_iter)?; // authority, must sign
    let ata_token_acc = next_account_info(acc_iter)?;

    if !owner_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Sanity: token account belongs to owner and matches mint
    let token_account = SplAccount::unpack(&ata_token_acc.try_borrow_data()?)?;
    if token_account.mint != *mint_account.key || token_account.owner != *owner_account.key {
        return Err(ProgramError::InvalidAccountData);
    }

    // Derive base units using mint decimals
    let mint = SplMint::unpack(&mint_account.try_borrow_data()?)?;
    let decimals = mint.decimals;
    let amount_base = amount_ui
        .checked_mul(10u64.pow(decimals as u32))
        .ok_or(ProgramError::InvalidArgument)?;

    // Burn (checked) — accounts: [token_account, mint, authority]
    let burn_ix = token_instruction::burn_checked(
        &spl_token_id(),
        ata_token_acc.key,
        mint_account.key,
        owner_account.key,
        &[],
        amount_base,
        decimals,
    )?;

    invoke(
        &burn_ix,
        &[
            ata_token_acc.clone(),
            mint_account.clone(),
            owner_account.clone(),
        ],
    )?;

    Ok(())
}

Conclusion

With the core pieces in place, mints that define the assets, ATAs that hold balances, and a burn mechanism to control supply. You’ve learnt how to create a mint as a PDA, initialize it with explicit decimals and authorities, derive and fund ATAs, mint and transfer in base units, and burn safely.

With these primitives, you can ship payments, points, or NFTs end-to-end in Native Rust.

You can find the code examples with associated tests in this GitHub repository. This is part one of a two-part series; the next part will cover Token-2022 extensions and hooks. To get notified when it drops, follow me on X :).


This content originally appeared on DEV Community and was authored by David Ekete


Print Share Comment Cite Upload Translate Updates
APA

David Ekete | Sciencx (2025-10-15T23:01:52+00:00) Working with Tokens on Solana using Native Rust. Retrieved from https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/

MLA
" » Working with Tokens on Solana using Native Rust." David Ekete | Sciencx - Wednesday October 15, 2025, https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/
HARVARD
David Ekete | Sciencx Wednesday October 15, 2025 » Working with Tokens on Solana using Native Rust., viewed ,<https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/>
VANCOUVER
David Ekete | Sciencx - » Working with Tokens on Solana using Native Rust. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/
CHICAGO
" » Working with Tokens on Solana using Native Rust." David Ekete | Sciencx - Accessed . https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/
IEEE
" » Working with Tokens on Solana using Native Rust." David Ekete | Sciencx [Online]. Available: https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/. [Accessed: ]
rf:citation
» Working with Tokens on Solana using Native Rust | David Ekete | Sciencx | https://www.scien.cx/2025/10/15/working-with-tokens-on-solana-using-native-rust-2/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.