Solana Programs Part 1
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 mintexpected_decimals: the expected mint decimal (for mint validation)
Note that there are several validity checks for the MintTo instruction:
-
The destination account is not frozen and is not native
-
The destination account’s mint is valid: it is the mint account’s key
-
The destination account’s owner and the mint account’s owner are both the token program
-
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:
-
Check if the source account’s delegate matches the user-supplied authority account (line 617) and that account is signed (lines 618–623).
-
Check if the burned
amountis no more than the source account’sdelegate_amount(line 625) -
Decrement the
delegate_amountby the burnedamount(lines 628–631), and ifdelegate_amountbecomes zero then set source account’sdelegatetoNone(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:
- Neither
source_accountnordestination_accountis frozen - The
source_account’s mint anddestination_account’s mint are the same - The transferred
amountis no more thansource_account’s token amount - the authority (either the owner of
source_accountor 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:
- transfer all its lamports to a destination account (lines 695–697)
- set its lamports to zero (line 700)
- call
sol_memsetto 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:
AuthorityType::AccountOwner: set a token account’s ownerAuthorityType::CloseAccount: set a token account’sclose_authorityAuthorityType::MintTokens: set a mint account’smint_authorityAuthorityType::FreezeAccount: set a mint account’sfree_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 flagsigners— 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