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

Solana Programs Part 1

Sec3 Research Team

Solana token program (source code) is among the most frequently executed Solana smart contracts. Its program id: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

  • Most user-deployed Solana smart contracts (directly or transitively) use the token program to mint/transfer/burn tokens (i.e., SPL tokens). For example, if your Solana project is a decentralised exchange, a stable coin, an ICO, or a cross-chain bridge, you are likely relying on the token program.

  • SPL tokens are similar to ERC20/ERC721 tokens, but with tricky differences.

In this article, we elaborate on the SPL tokens and introduce the internals of those most commonly used instructions in the token program:

Instruction

SPL Function

InitializeMint

spl_token::instruction::initialize_mint

InitializeAccount

spl_token::instruction::initialize_account

MintTo

spl_token::instruction::mint_to

Burn

spl_token::instruction::burn

Transfer

spl_token::instruction::transfer

SyncNative

spl_token::instruction::sync_native

Approve

spl_token::instruction::approve

Revoke

spl_token::instruction::revoke

FreezeAccount

spl_token::instruction::freeze_account

ThawAccount

spl_token::instruction::thaw_account

CloseAccount

spl_token::instruction::close_account

SetAuthority

spl_token::instruction::set_authority

These instructions are implemented with three important data structures:

  • Mint — data type of every mint account (e.g. mint_info.data)

  • Account — data type of every token account (e.g. source_account_info.data)

  • Multisig — data type of every multisig account (e.g. owner_account_info.data)

The InitializeMint Instruction

The InitializeMint instruction is the first instruction to call before anything else in the SPL token mint process. It will invoke the function _process_initialize_mint to initialize a mint account (on line 36 mint_info supplied as the first account in the accounts vector):

    fn _process_initialize_mint(
        accounts: &[AccountInfo],
        decimals: u8,
        mint_authority: Pubkey,
        freeze_authority: COption<Pubkey>,
        rent_sysvar_account: bool,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let mint_info: &AccountInfo = next_account_info(account_info_iter)?;

The mint account’s data is deserialized into mint of the Mint data type, and then checked for re-initialization error:

        let mut mint: Mint = Mint::unpack_unchecked(input: &mint_info.data.borrow())?;
        if mint.is_initialized {
            return Err(TokenError::AlreadyInUse.into());

The Mint struct has five attributes:

    pub struct Mint {
        /// Optional authority used to mint new tokens. The mint authority may only be provided during
        /// mint creation. If no mint authority is present then the mint has a fixed supply and no
        /// further tokens may be minted.
        pub mint_authority: COption<Pubkey>,
        /// Total supply of tokens.
        pub supply: u64,
        /// Number of base 10 digits to the right of the decimal place.
        pub decimals: u8,
        /// Is `true` if this structure has been initialized
        pub is_initialized: bool,
        /// Optional authority to freeze token accounts.
        pub freeze_authority: COption<Pubkey>,

Finally, the mint account is initialized by setting its mint_authority , decimals , is_initialized flag true, and an optional freeze_authority.

        mint.mint_authority = COption::Some(mint_authority);
        mint.decimals = decimals;
        mint.is_initialized = true;
        mint.freeze_authority = freeze_authority;

Note that in the initialization code above, the mint’s supply is not set but has a default value 0.

The InitializeAccount Instruction

The instruction will invoke the function _process_initialize_account to initialize a token account (on line 90 new_account_info supplied as the first account in the accounts vector):

    fn _process_initialize_account(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        owner: Option<&Pubkey>,
        rent_sysvar_account: bool,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let new_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let mint_info: &AccountInfo = next_account_info(account_info_iter)?;
        let owner: &Pubkey = if let Some(owner: &Pubkey) = owner {
            owner
        } else {
            next_account_info(account_info_iter)?.key

Similar to InitializeMint, the token account’s data is deserialized into account of the Token data type, and then checked for re-initialization error:

        let mut account: Account = Account::unpack_unchecked(input: &new_account_info.data.borrow())?;
        if account.is_initialized() {
            return Err(TokenError::AlreadyInUse.into());

The Token struct has eight attributes:

    pub struct Account {
        /// The mint associated with this account
        pub mint: Pubkey,
        /// The owner of this account.
        pub owner: Pubkey,
        /// The amount of tokens this account holds.
        pub amount: u64,
        /// If `delegate` is `Some` then `delegated_amount` represents
        /// the amount authorized by the delegate
        pub delegate: COption<Pubkey>,
        /// The account's state
        pub state: AccountState,
        /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
        /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
        /// wrapped SOL accounts do not drop below this threshold.
        pub is_native: COption<u64>,
        /// The amount delegated
        pub delegated_amount: u64,
        /// Optional authority to close the account.
        pub close_authority: COption<Pubkey>,

Finally, the token account is initialized by setting its mint, owner, close_authority (default None), delegate (default None), delegated_amount (default 0), state (Initialized), is_native flag and amount(i.e. the amount of tokens this account holds):

        account.mint = *mint_info.key;
        account.owner = *owner;
        account.close_authority = COption::None;
        account.delegate = COption::None;
        account.delegated_amount = 0;
        account.state = AccountState::Initialized;
        if is_native_mint {
            let rent_exempt_reserve: u64 = rent.minimum_balance(new_account_info_data_len);
            account.is_native = COption::Some(rent_exempt_reserve);
            account.amount = new_account_info &AccountInfo
                .lamports() u64
                .checked_sub(rent_exempt_reserve) Option<u64>
                .ok_or(err: TokenError::Overflow)?;
        } else {
            account.is_native = COption::None;
            account.amount = 0;
        let is_native_mint: bool = Self::cmp_pubkeys(a: mint_info.key, b: &crate::native_mint::id());

Note that when the token mint is the native mint, i.e., WSOL (the wrapped sol, program_id: So11111111111111111111111111111111111111112), is_native is true and amount is initialized to the amount of lamports (minus rent) in the token account.

The MintTo Instruction

After a mint and a token account have been initialized, the MintTo instruction can be called to mint tokens by function process_mint_to:

    pub fn process_mint_to(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        amount: u64,
        expected_decimals: Option<u8>,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let mint_info: &AccountInfo = next_account_info(account_info_iter)?;
        let destination_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let owner_info: &AccountInfo = next_account_info(account_info_iter)?;

The MintTo instruction takes three user-supplied accounts:

  • mint_info: the mint account (line 523)
  • destination_account_info : the token account to mint tokens to (line 524)
  • owner_info : the mint account’s authority (line 525)

It also takes two user-supplied instruction data:

  • amount : the amount of tokens to mint
  • expected_decimals : the expected mint decimal (for mint validation)

Note that there are several validity checks for the MintTo instruction:

  1. The destination account is not frozen and is not native

  2. The destination account’s mint is valid: it is the mint account’s key

  3. The destination account’s owner and the mint account’s owner are both the token program

  4. The transaction is signed by the mint account’s authority

        match mint.mint_authority {
            COption::Some(mint_authority: Pubkey) => Self::validate_owner(
                program_id,
                expected_owner: &mint_authority,
                owner_account_info: owner_info,
                signers: account_info_iter.as_slice(),
            )?,

Finally, if the MintTo instruction is processed successfully, the destination account’s token amount (destination_account.amount) and the mint’s supply (mint.supply) will both be incremented by amount :

        destination_account.amount = destination_account // Account
            .amount // u64
            .checked_add(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;

        mint.supply = mint // Mint
            .supply // u64
            .checked_add(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;

The Burn Instruction

The Burn instruction is (mostly) opposite to MintTo . It calls the function process_burn to burn a certain amount of tokens from a source token account:

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

        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let mint_info: &AccountInfo = next_account_info(account_info_iter)?;
        let authority_info: &AccountInfo = next_account_info(account_info_iter)?;

If the Burn instruction is successful, the source account’s token amount (source_account.amount) and the mint’s supply (mint.supply) will both be decremented by amount :

        source_account.amount = source_account // Account
            .amount // u64
            .checked_sub(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;
        mint.supply = mint // Mint
            .supply // u64
            .checked_sub(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;

A main difference is that the signed authority account is not the mint’s authority, but the source account’s owner or delegate. For the latter, the logic is slightly complex:

  1. Check if the source account’s delegate matches the user-supplied authority account (line 617) and that account is signed (lines 618–623).

  2. Check if the burned amount is no more than the source account’s delegate_amount (line 625)

  3. Decrement the delegate_amount by the burned amount (lines 628–631), and if delegate_amount becomes zero then set source account’s delegate to None (lines 632–633).

        match source_account.delegate {
            COption::Some(ref delegate: &Pubkey) if Self::cmp_pubkeys(a: authority_info.key, b: delegate) => {
                Self::validate_owner(
                    program_id,
                    expected_owner: delegate,
                    owner_account_info: authority_info,
                    signers: account_info_iter.as_slice(),
                )?;

                if source_account.delegated_amount < amount {
                    return Err(TokenError::InsufficientFunds.into());
                }
                source_account.delegated_amount = source_account // Account
                    .delegated_amount // u64
                    .checked_sub(amount) // Option<u64>
                    .ok_or(err: TokenError::Overflow)?;
                if source_account.delegated_amount == 0 {
                    source_account.delegate = COption::None;

The Transfer Instruction

The Transfer instruction will invoke the function process_transfer to transfer a certain amount of token from a source account to a destination account:

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

        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let destination_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let authority_info: &AccountInfo = next_account_info(account_info_iter)?;

This function has several important validity checks:

  1. Neither source_account nor destination_account is frozen
  2. The source_account’s mint and destination_account’s mint are the same
  3. The transferred amount is no more than source_account’s token amount
  4. the authority (either the owner of source_account or its matched delegate) is signed

> Note: source_account may be the same as destination_account . If that’s the case, then it is a self-transfer, and the function simply returns Ok. In other words, self-transfer is allowed by the token program**.**

Finally, if the Transfer instruction is successful, the source_account’s token amount is decremented by amount and the destination_account’s token amount is incremented by amount :

        source_account.amount = source_account // Account
            .amount // u64
            .checked_sub(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;
        destination_account.amount = destination_account // Account
            .amount // u64
            .checked_add(amount) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;

Note: For native token accounts (i.e., the token mint is WSOL), in order to transfer SOL directly using the token program, the token amount should be “sync-ed” with the amount of lamports in the account.

Thus, the lamports of source_account and destination_account must also be updated accordingly:

        if source_account.is_native() {
            let source_starting_lamports: u64 = source_account_info.lamports();
            **source_account_info.lamports.borrow_mut() = source_starting_lamports // u64
                .checked_sub(amount) // Option<u64>
                .ok_or(err: TokenError::Overflow)?;

            let destination_starting_lamports: u64 = destination_account_info.lamports();
            **destination_account_info.lamports.borrow_mut() = destination_starting_lamports // u64
                .checked_add(amount) // Option<u64>
                .ok_or(err: TokenError::Overflow)?;

The SyncNative Instruction

For a native token account, when the token amount is less than the amount of lamports in the account (possibly caused by a system_instruction transfer of lamports to the token account), the token program has a SyncNative instruction to sync these two values:

    pub fn process_sync_native(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let native_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        Self::check_account_owner(program_id, native_account_info)?;

        let mut native_account: Account = Account::unpack(input: &native_account_info.data.borrow())?;

        if let COption::Some(rent_exempt_reserve: u64) = native_account.is_native {
            let new_amount: u64 = native_account_info // &AccountInfo
                .lamports() // u64
                .checked_sub(rent_exempt_reserve) // Option<u64>
                .ok_or(err: TokenError::Overflow)?;
            if new_amount < native_account.amount {
                return Err(TokenError::InvalidState.into());
            }
            native_account.amount = new_amount;
        } else {
            return Err(TokenError::NonNativeNotSupported.into());

In the above, the native account’s amount is updated to the amount of lamports in the account minus rent (line 768).

The Approve Instruction

The Approve instruction will invoke the function process_approve to set a delegate account for a source account and a delegate amount by the source account’s authority:

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

        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        source_account.delegate = COption::Some(*delegate_info.key);
        source_account.delegated_amount = amount;

Note that a token account allows only one delegate at a time.

> The Approve instruction will override the previous delegate and the delegate amount set by the last Approve instruction. Even if the previous delegate amount is not spent or only spent partially, it will still be overwritten.

The Revoke Instruction

The Revoke instruction will call the function process_revoke to set a source account’s delegate to None and the delegate amount to 0:

    pub fn process_revoke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let mut source_account: Account = Account::unpack(input: &source_account_info.data.borrow())?;
        let owner_info: &AccountInfo = next_account_info(account_info_iter)?;
        source_account.delegate = COption::None;
        source_account.delegated_amount = 0;

The FreezeAccount and ThawAccount Instructions

The FreezeAccount and ThawAccount instructions will call the function process_toggle_freeze_account to set a source account’s state to Frozen and Initialized respectively:

    pub fn process_toggle_freeze_account(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        freeze: bool,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let mint_info: &AccountInfo = next_account_info(account_info_iter)?;
        let authority_info: &AccountInfo = next_account_info(account_info_iter)?;
        source_account.state = if freeze {
            AccountState::Frozen
        } else {
            AccountState::Initialized

Note that this instruction must be signed by the freeze_authority of the mint account (line 716 mint_info). It will fail if the mint’s free_authority is not set.

The CloseAccount Instruction

The CloseAccount instruction will call the function process_close_account to close a token account (source_account) and transfer all its lamports to another token account (destination_account) by the source code’s authority:

    pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let source_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let destination_account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let authority_info: &AccountInfo = next_account_info(account_info_iter)?;

The close of a token account includes three steps:

  1. transfer all its lamports to a destination account (lines 695–697)
  2. set its lamports to zero (line 700)
  3. call sol_memset to clear the account’s data (line 702)
        let destination_starting_lamports: u64 = destination_account_info.lamports();
        **destination_account_info.lamports.borrow_mut() = destination_starting_lamports // u64
            .checked_add(source_account_info.lamports()) // Option<u64>
            .ok_or(err: TokenError::Overflow)?;

        **source_account_info.lamports.borrow_mut() = 0;

        sol_memset(s: *source_account_info.data.borrow_mut(), c: 0, n: Account::LEN);

Note that the source account must be not the same as the destination account:

        if Self::cmp_pubkeys(a: source_account_info.key, b: destination_account_info.key) {
            return Err(ProgramError::InvalidAccountData);

The SetAuthority Instruction

The SetAuthority instruction is used to set a new authority for a mint or token account. The authority could be either a single account (wallet or PDA) or a Multisignature account (will elaborate shortly).

    pub fn process_set_authority(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        authority_type: AuthorityType,
        new_authority: COption<Pubkey>,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let account_info: &AccountInfo = next_account_info(account_info_iter)?;
        let authority_info: &AccountInfo = next_account_info(account_info_iter)?;

The type of authority to set is specified by the authority_type . There are four different types:

  1. AuthorityType::AccountOwner : set a token account’s owner
  2. AuthorityType::CloseAccount : set a token account’s close_authority
  3. AuthorityType::MintTokens : set a mint account’s mint_authority
  4. AuthorityType::FreezeAccount : set a mint account’s free_authority

The Multisig Structure

A Multisig account has the Multisig data type with four attributes:

  • m — number of required signers
  • n —total number of valid signers
  • is_initialized — initialization flag
  • signers — an array of all valid signer pubkeys
    pub struct Multisig {
        /// Number of signers required
        pub m: u8,
        /// Number of valid signers
        pub n: u8,
        /// Is `true` if this structure has been initialized
        pub is_initialized: bool,
        /// Signer public keys
        pub signers: [Pubkey; MAX_SIGNERS],

A Multisig account can be initialized by the InitializeMultisig instruction (line 175 multisig_info account):

    fn _process_initialize_multisig(
        accounts: &[AccountInfo],
        m: u8,
        rent_sysvar_account: bool,
    ) -> ProgramResult {
        let account_info_iter: &mut Iter<AccountInfo> = &mut accounts.iter();
        let multisig_info: &AccountInfo = next_account_info(account_info_iter)?;

In the initialization, m is passed as a parameter, n is the length of valid signer accounts (line 192 signer_infos supplied in the accounts vector):

        let signer_infos: &[AccountInfo] = account_info_iter.as_slice();
        multisig.m = m;
        multisig.n = signer_infos.len() as u8;
        if !is_valid_signer_index(multisig.n as usize) {
            return Err(TokenError::InvalidNumberOfProvidedSigners.into());
        }
        if !is_valid_signer_index(multisig.m as usize) {
            return Err(TokenError::InvalidNumberOfRequiredSigners.into());
        }
        for (i: usize, signer_info: &AccountInfo) in signer_infos.iter().enumerate() {
            multisig.signers[i] = *signer_info.key;
        }
        multisig.is_initialized = true;

We will continue to introduce the technical details of other popular Solana programs (e.g., token-swap, stake-pool, associated-token-account) in the next few articles.


About sec3 (Formerly Soteria)

sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with GitHub to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.

To learn more about sec3, please visit https://www.sec3.dev

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