Overview
Solana’s fungible token implementation is quite different from Ethereum’s. This orgins from the architecure difference between Solana and Ethereum.
Solana is a high-performance blockchain platform designed to support decentralized applications (dApps) and crypto-currencies at scale. It aims to offer fast transaction speeds and high throughput. Solana achieves this through a combination of innovations, including Proof of History (PoH), Tower BFT (a PoS consensus algorithm optimized for scalability), and a highly efficient parallel processing engine called Sealevel.
Solana Compared To Ethereum
Consensus Mechanism
Ethereum
Use Proof of Stake (PoS) in Ethereum 2.0, previously used Proof of Work (PoW) in Ethereum 1.0. PoS allows validators to create new blocks and validate transactions based on the number of tokens they hold and are willing to "stake" as collateral.
Solana
Utilizes Proof of History (PoH) to create a historical record that proves that an event has occurred at a specific moment in time. This is combined with Tower BFT, a consensus algorithm that optimizes for performance and energy efficiency.
Transaction Speed and Throughput
Ethereum
Current Ethereum 2.0 aims to significantly increase throughput, but traditionally, Ethereum 1.0 could handle around 15-30 TPS.
Solana
Can handle up to 65,000 transactions per second (TPS) due to its highly optimized architecture and parallel processing capabilities. In Solana, each transaction needs to specify the accounts involved, including their types and roles, like whethe the account is to be read, write or is signer. This helps SVM to group transaction which has no writer operation overlap to execute transactions in parallel.
Smart Contract Execution
Ethereum
Uses the Ethereum Virtual Machine (EVM) to process smart contracts in a sequential manner. Ethereum 2.0 and future upgrades aim to introduce more parallelism.
Solana
Uses a novel execution model where transactions are processed in parallel across multiple chains (Sealevel). This allows for greater efficiency and higher throughput.
Account Model Difference
Ethereum
- Ethereum uses two types of accounts: Externally Owned Accounts (EOAs) and Contract Accounts.
- EOA is controlled by private keys, can send transactions and hold balances.
- Contract Accounts is controlled by smart contract code, can execute code when triggered by a transaction from an EOA or another contract.
Solana
- Accounts in solana are either executable (smart contracts/program) or non-executable (data storage).
- Accounts can be owned by programs, which manage the account's data and execution.
- The account model is optimized for parallelism, allowing multiple accounts to be read and written simultaneously without conflicts.
Batch Transaction
Ethereum
Only one instruction can be executed in one transaction.
Solana
Multiple instructions can be executed in one transaction atomically.
ERC20 Token Model
Ethereum
On Ethereum, each ERC20 token has a smart contract. And the token ownership data is stored in the single smart contract. This is straight forward but not designed for parallel execution. Think about and account both issue an transaction transfers some token to another address, the EVM needs to execute those two transactions by order, because EVM has no knowledge about whether those two transactions will modify the same data which will incur data race if executed simultaneously.
Solana
On solana, to support parallel execution, users’ token ownership data is stored on separate account respectively in spl token standard.
By specifying which account will be read or written in the tx, SVM can differentiate transactions which have no write operation overlap, and execute them simultaneously and correctly.
Due to the improvements in the token model, the throughput of token-related operations in Solana is much faster than in Ethereum.
Both SPL and ERC20 tokens manage token supply, support transfers, and allow for the burning of tokens.
SPL Token Model Architecture
In solana, there is a deployed general purpose Token Program which implement logic to handle all token related operations, like
mint
, transfer
, approve
, burn
, etc.Metadata of token is stored in a
mint
account, includes:- mint authority:
who is able to mint token to other accounts.
- token supply:
total supply of this token.
- decimals:
- freeze_authority:
the account who is able to freeze token account. Freezed token accounts can’t transfer token to other accounts.
Wallet’s token ownership information regarding certain token is stored in token account separately, those information includes:
- Corresponding token address(
mint
account’s address)
- Token account’s owner address
Owner controls the token account, having the authority to transfer, approve token, etc.
- token account’s delegate address
Delegate is approved by token account owner, has authority to transfer and burn approved amount of token.
- token account’s current state
Like whether the token account has been initialized.
- whether this token account is used to store native token lamport.
Token account stores lamport is similar to wrapped token in Ethereum, like WETH.
- delegated amount
The amount of token token that the account delegate is able to use.
- close authority
Account which is able to close the token account.
The purpose of Solana's token model design is to separate data as much as possible to improve the efficiency of parallel execution.
Create And Initialize Mint
spl-token library constructs instruction to create mint account
In the Solana SPL Token standard, the
mint
account is a special type of account that stores the metadata for a specific token. Definition of
Mint
account in SPL:/// ---token/program/src/state.rs--- #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] 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>, }
To deploy a
Mint
account:- first, deploy an common account
- second, initialize it as an
Mint
account.
spl-token
is a library which helps manage spl token, we can see the createMint
is the function to create Mint account
. createMint
creates a tx consists of two instructions, the first instruction is to call SystemProgram.createAccount
to create a new account(Mint
), the second is to initialize the Mint
account./// ---@solana/spl-token/src/actions/createMint.ts--- /** * Create and initialize a new mint * * @param connection Connection to use * @param payer Payer of the transaction and initialization fees * @param mintAuthority Account or multisig that will control minting * @param freezeAuthority Optional account or multisig that can freeze token accounts * @param decimals Location of the decimal place * @param keypair Optional keypair, defaulting to a new random one * @param confirmOptions Options for confirming the transaction * @param programId SPL Token program account * * @return Address of the new mint */ export async function createMint( connection: Connection, payer: Signer, mintAuthority: PublicKey, freezeAuthority: PublicKey | null, decimals: number, keypair = Keypair.generate(), confirmOptions?: ConfirmOptions, programId = TOKEN_PROGRAM_ID ): Promise<PublicKey> { const lamports = await getMinimumBalanceForRentExemptMint(connection); const transaction = new Transaction().add( SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: keypair.publicKey, space: MINT_SIZE, lamports, programId, }), createInitializeMint2Instruction(keypair.publicKey, decimals, mintAuthority, freezeAuthority, programId) ); await sendAndConfirmTransaction(connection, transaction, [payer, keypair], confirmOptions); return keypair.publicKey; }
create account
SystemProgram.createAccount
constructs an instruction which calls system program(11111111111111111111111111111111) to create a new account. /// ---/@solana/web3.js/lib/index.cjs.js--- /** * Generate a transaction instruction that creates a new account */ static createAccount(params) { const type = SYSTEM_INSTRUCTION_LAYOUTS.Create; const data = encodeData(type, { lamports: params.lamports, space: params.space, programId: toBuffer(params.programId.toBuffer()) }); return new TransactionInstruction({ keys: [{ pubkey: params.fromPubkey, isSigner: true, isWritable: true }, { pubkey: params.newAccountPubkey, isSigner: true, isWritable: true }], programId: this.programId, data });
create Initialize Mint Instruction
createInitializeMint2Instruction
constructs an instruction which calls TOKEN_PROGRAM
to initialize the Mint
account./// ---/@solana/spl-token/src/instructions/initializeMint2.ts--- /** * Construct an InitializeMint2 instruction * * @param mint Token mint account * @param decimals Number of decimals in token account amounts * @param mintAuthority Minting authority * @param freezeAuthority Optional authority that can freeze token accounts * @param programId SPL Token program account * * @return Instruction to add to a transaction */ export function createInitializeMint2Instruction( mint: PublicKey, decimals: number, mintAuthority: PublicKey, freezeAuthority: PublicKey | null, programId = TOKEN_PROGRAM_ID ): TransactionInstruction { const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; const data = Buffer.alloc(initializeMint2InstructionData.span); initializeMint2InstructionData.encode( { instruction: TokenInstruction.InitializeMint2, decimals, mintAuthority, freezeAuthorityOption: freezeAuthority ? 1 : 0, freezeAuthority: freezeAuthority || new PublicKey(0), }, data ); return new TransactionInstruction({ keys, programId, data }); }
System program creates new account
system_processor
is the entry of system program.InvokeContext
contains the context needed to execute a transaction.- It starts by retrieving the
transaction_context
andinstruction_context
.
- It then retrieves and deserializes the
instruction_data
into aSystemInstruction
When the instruction is
CreateAccount
it performs the following:- Checks the number of instruction accounts to ensure there are at least two. Remember the instruction data we created using solana/web3.js? The first account the
from
account which is to pay lamport to initialize the new account. the second account is the new account to be created.
- Retrieves the account address (
to_address
) from the transaction context.to_address
it the address of tha account to be created.
- Calls
create_account
function to handle the account creation.
/// ---programs/system/src/system_processor.rs--- pub const DEFAULT_COMPUTE_UNITS: u64 = 150; pub struct InvokeContext<'a> { pub transaction_context: &'a mut TransactionContext, sysvar_cache: &'a SysvarCache, log_collector: Option<Rc<RefCell<LogCollector>>>, compute_budget: ComputeBudget, current_compute_budget: ComputeBudget, compute_meter: RefCell<u64>, pub programs_loaded_for_tx_batch: &'a LoadedProgramsForTxBatch, pub programs_modified_by_tx: &'a mut LoadedProgramsForTxBatch, pub feature_set: Arc<FeatureSet>, pub timings: ExecuteDetailsTimings, pub blockhash: Hash, pub lamports_per_signature: u64, pub syscall_context: Vec<Option<SyscallContext>>, traces: Vec<Vec<[u64; 12]>>, } declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| { let transaction_context = &invoke_context.transaction_context; let instruction_context = transaction_context.get_current_instruction_context()?; let instruction_data = instruction_context.get_instruction_data(); // decode instruction byte array to enum SystemInstruction let instruction = limited_deserialize(instruction_data)?; trace!("process_instruction: {:?}", instruction); let signers = instruction_context.get_signers(transaction_context)?; match instruction { SystemInstruction::CreateAccount { lamports, space, owner, } => { // check there are two accounts in the instruction context. // One is the account to pay lamport to initialize new account, // the other is the new account to be created. instruction_context.check_number_of_instruction_accounts(2)?; // get the address of the account to be created let to_address = Address::create( transaction_context.get_key_of_account_at_index( instruction_context.get_index_of_instruction_account_in_transaction(1)?, )?, None, invoke_context, )?; create_account( 0, 1, &to_address, lamports, space, &owner, &signers, invoke_context, transaction_context, instruction_context, ) } } // ... }
create_account
create_account
handles the creation of a new account. It requires two signer accounts to make the account creation operation succeeds. One is account which is to be created, the other is payer account pays the lamport to initialize the to-be-created account. And it allows to specify an program to be the owner of the newly created account.Process:
- Account Validation:
- It first checks if the
to
account is already in use by checking its lamports. If the account has lamports, it logs a message and returns error.
- Account Allocation and Assignment:
- If the account is not in use, it calls
allocate_and_assign
to allocate space and assign ownership.
- Transfer Lamports:
- After allocating and assigning the account, it calls
transfer
to move lamports from thefrom
account to theto
account. This is ususally used to make the newly created account rent exempt.
/// ---programs/system/src/system_processor.rs--- #[allow(clippy::too_many_arguments)] fn create_account( from_account_index: IndexOfAccount, to_account_index: IndexOfAccount, to_address: &Address, lamports: u64, space: u64, owner: &Pubkey, signers: &HashSet<Pubkey>, invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, ) -> Result<(), InstructionError> { // if it looks like the `to` account is already in use, bail { let mut to = instruction_context .try_borrow_instruction_account(transaction_context, to_account_index)?; if to.get_lamports() > 0 { ic_msg!( invoke_context, "Create Account: account {:?} already in use", to_address ); return Err(SystemError::AccountAlreadyInUse.into()); } allocate_and_assign(&mut to, to_address, space, owner, signers, invoke_context)?; } transfer( from_account_index, to_account_index, lamports, invoke_context, transaction_context, instruction_context, ) }
allocate_and_assign
allocate_and_assign
allocates space to the newly create account, and update the owner./// ---programs/system/src/system_processor.rs--- fn allocate_and_assign( to: &mut BorrowedAccount, to_address: &Address, space: u64, owner: &Pubkey, signers: &HashSet<Pubkey>, invoke_context: &InvokeContext, ) -> Result<(), InstructionError> { allocate(to, to_address, space, signers, invoke_context)?; assign(to, to_address, owner, signers, invoke_context) }
allocate
handles the space allocation for the new account.Process:
- Signer Verification:
Checks if the
to
account is signed by the required signers using address.is_signer(signers)
. - Account Usage Check:
Checks if the account is already in use by verifying if it has any data or if it is owned by a system program using
!account.get_data().is_empty()
and !system_program::check_id(account.get_owner())
. - Space Validation:
Checks if the requested space exceeds the maximum permitted data length (
MAX_PERMITTED_DATA_LENGTH
).- Data Length Setting:
Sets the data length of the account to the requested space using
account.set_data_length(space as usize)
.set_data_length
:can_data_be_changed
handles validation check:- the account whose data to be modified can’t be executable.
- the account whose data to be modified should be writable.
- only owner program can modify data of account.
set_data_length
:self.account.resize
fills the data space with value 0./// ---programs/system/src/system_processor.rs--- /// Maximum permitted size of account data (10 MiB). pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024; fn allocate( account: &mut BorrowedAccount, address: &Address, space: u64, signers: &HashSet<Pubkey>, invoke_context: &InvokeContext, ) -> Result<(), InstructionError> { if !address.is_signer(signers) { ic_msg!( invoke_context, "Allocate: 'to' account {:?} must sign", address ); return Err(InstructionError::MissingRequiredSignature); } // if it looks like the `to` account is already in use, bail // (note that the id check is also enforced by message_processor) if !account.get_data().is_empty() || !system_program::check_id(account.get_owner()) { ic_msg!( invoke_context, "Allocate: account {:?} already in use", address ); return Err(SystemError::AccountAlreadyInUse.into()); } if space > MAX_PERMITTED_DATA_LENGTH { ic_msg!( invoke_context, "Allocate: requested {}, max allowed {}", space, MAX_PERMITTED_DATA_LENGTH ); return Err(SystemError::InvalidAccountDataLength.into()); } account.set_data_length(space as usize)?; Ok(()) } /// Resizes the account data (transaction wide) /// /// Fills it with zeros at the end if is extended or truncates at the end otherwise. #[cfg(not(target_os = "solana"))] pub fn set_data_length(&mut self, new_length: usize) -> Result<(), InstructionError> { self.can_data_be_resized(new_length)?; self.can_data_be_changed()?; // don't touch the account if the length does not change if self.get_data().len() == new_length { return Ok(()); } self.touch()?; self.update_accounts_resize_delta(new_length)?; self.account.resize(new_length, 0); Ok(()) } /// Returns an error if the account data can not be mutated by the current program #[cfg(not(target_os = "solana"))] pub fn can_data_be_changed(&self) -> Result<(), InstructionError> { // Only non-executable accounts data can be changed if self.is_executable() { return Err(InstructionError::ExecutableDataModified); } // and only if the account is writable if !self.is_writable() { return Err(InstructionError::ReadonlyDataModified); } // and only if we are the owner if !self.is_owned_by_current_program() { return Err(InstructionError::ExternalAccountDataModified); } Ok(()) } // ---sdk/src/account.rs--- pub fn resize(&mut self, new_len: usize, value: u8) { self.data_mut().resize(new_len, value) } // --rustlib---/// /// let mut vec = vec!["hello"]; /// vec.resize(3, "world"); /// assert_eq!(vec, ["hello", "world", "world"]); /// /// let mut vec = vec![1, 2, 3, 4]; /// vec.resize(2, 0); /// assert_eq!(vec, [1, 2]); /// ``` #[cfg(not(no_global_oom_handling))] #[stable(feature = "vec_resize", since = "1.5.0")] pub fn resize(&mut self, new_len: usize, value: T) { let len = self.len(); if new_len > len { self.extend_with(new_len - len, value) } else { self.truncate(new_len); } }
assign
function handles assigning an owner to the new account. It performs the following steps:- Owner Check:
Check whether the account's current owner is already the specified owner.
- Signer Verification:
Checks if the
to
account is signed by the required signers using address.is_signer(signers)
. address.is_signer
checks whether the account’s Pubkey
is signer or the base
account is signer is the account is derived from a base
account.- Owner Assignment:
Sets the owner of the account to the specified owner.
/// ---programs/system/src/system_processor.rs--- fn assign( account: &mut BorrowedAccount, address: &Address, owner: &Pubkey, signers: &HashSet<Pubkey>, invoke_context: &InvokeContext, ) -> Result<(), InstructionError> { // no work to do, just return if account.get_owner() == owner { return Ok(()); } if !address.is_signer(signers) { ic_msg!(invoke_context, "Assign: account {:?} must sign", address); return Err(InstructionError::MissingRequiredSignature); } account.set_owner(&owner.to_bytes()) } // represents an address that may or may not have been generated // from a seed #[derive(PartialEq, Eq, Default, Debug)] struct Address { address: Pubkey, base: Option<Pubkey>, } impl Address { fn is_signer(&self, signers: &HashSet<Pubkey>) -> bool { if let Some(base) = self.base { signers.contains(&base) } else { signers.contains(&self.address) } } // ... }
set_owner
update the account’s owner. It checks:- the account is not owned by current executing program.
- the ccount is writable.
- the account is not executable(is data account).
- the account must be zeroed.
/// ---sdk/src/transaction_context.rs--- /// Assignes the owner of this account (transaction wide) #[cfg(not(target_os = "solana"))] pub fn set_owner(&mut self, pubkey: &[u8]) -> Result<(), InstructionError> { // Only the owner can assign a new owner if !self.is_owned_by_current_program() { return Err(InstructionError::ModifiedProgramId); } // and only if the account is writable if !self.is_writable() { return Err(InstructionError::ModifiedProgramId); } // and only if the account is not executable if self.is_executable() { return Err(InstructionError::ModifiedProgramId); } // and only if the data is zero-initialized or empty if !is_zeroed(self.get_data()) { return Err(InstructionError::ModifiedProgramId); } // don't touch the account if the owner does not change if self.get_owner().to_bytes() == pubkey { return Ok(()); } self.touch()?; self.account.copy_into_owner_from_slice(pubkey); Ok(()) } /// ---sdk/src/transaction_context.rs--- #[cfg(not(target_os = "solana"))] fn is_zeroed(buf: &[u8]) -> bool { const ZEROS_LEN: usize = 1024; const ZEROS: [u8; ZEROS_LEN] = [0; ZEROS_LEN]; let mut chunks = buf.chunks_exact(ZEROS_LEN); #[allow(clippy::indexing_slicing)] { chunks.all(|chunk| chunk == &ZEROS[..]) && chunks.remainder() == &ZEROS[..chunks.remainder().len()] } }
transfer
transfer
function handles the transfer of lamports from one account to another. Process:
- Signature Verification:
check if the
from
account has signed the transaction- Transfer Verification:
If the signature verification passes, the function calls
transfer_verified
to perform the actual transfer./// ---programs/system/src/system_processor.rs--- fn transfer( from_account_index: IndexOfAccount, to_account_index: IndexOfAccount, lamports: u64, invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, ) -> Result<(), InstructionError> { if !instruction_context.is_instruction_account_signer(from_account_index)? { ic_msg!( invoke_context, "Transfer: `from` account {} must sign", transaction_context.get_key_of_account_at_index( instruction_context .get_index_of_instruction_account_in_transaction(from_account_index)?, )?, ); return Err(InstructionError::MissingRequiredSignature); } transfer_verified( from_account_index, to_account_index, lamports, invoke_context, transaction_context, instruction_context, ) }
transfer_verified
performs the actual transfer of lamports between accounts after all necessary verifications. Key steps:
- borrow
from
andto
accounts.
- check
from
’s data is empty.(So program and data account can’t pay lamport to create new account, only wallet account can pay).
- check
from
has enough lamports.
- transfer lamports from
from
toto
.
/// ---programs/system/src/system_processor.rs--- fn transfer_verified( from_account_index: IndexOfAccount, to_account_index: IndexOfAccount, lamports: u64, invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, ) -> Result<(), InstructionError> { // borrow from let mut from = instruction_context .try_borrow_instruction_account(transaction_context, from_account_index)?; // verify from'data is empty if !from.get_data().is_empty() { ic_msg!(invoke_context, "Transfer: `from` must not carry data"); return Err(InstructionError::InvalidArgument); } // verify from has enough lamports if lamports > from.get_lamports() { ic_msg!( invoke_context, "Transfer: insufficient lamports {}, need {}", from.get_lamports(), lamports ); return Err(SystemError::ResultWithNegativeLamports.into()); } // substract from's lamports from.checked_sub_lamports(lamports)?; drop(from); // borrow to let mut to = instruction_context .try_borrow_instruction_account(transaction_context, to_account_index)?; // add to's lamports to.checked_add_lamports(lamports)?; Ok(()) }
Initialize Mint
In the spl-token/createMint, we can see the second instruction is to call
TOKEN_PROGRAM
's process_initialize_mint2
to initialize the newly creater Mint
account.In Solana, the Token Program is a system-level program provided by the Solana blockchain that facilitates the creation, transfer, and management of fungible tokens and non-fungible tokens (NFTs). It is a standardized, widely-used program that provides essential functionalities for token operations.
/// ---/@solana/spl-token/src/instructions/initializeMint2.ts--- /** * Construct an InitializeMint2 instruction * * @param mint Token mint account * @param decimals Number of decimals in token account amounts * @param mintAuthority Minting authority * @param freezeAuthority Optional authority that can freeze token accounts * @param programId SPL Token program account * * @return Instruction to add to a transaction */ export function createInitializeMint2Instruction( mint: PublicKey, decimals: number, mintAuthority: PublicKey, freezeAuthority: PublicKey | null, programId = TOKEN_PROGRAM_ID ): TransactionInstruction { const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; const data = Buffer.alloc(initializeMint2InstructionData.span); initializeMint2InstructionData.encode( { instruction: TokenInstruction.InitializeMint2, decimals, mintAuthority, freezeAuthorityOption: freezeAuthority ? 1 : 0, freezeAuthority: freezeAuthority || new PublicKey(0), }, data ); return new TransactionInstruction({ keys, programId, data }); }
initialize_mint2 constructs instruction
Besides @solana/spl-token, Token Program also provides
initialize_mint2
to construct InitializeMint2
instruction. So we can actually use program to construct and execute Mint
initialization.Parameters
token_program_id
: The public key of the Token Program. This indicates which program will process the instruction.
mint_pubkey
: The public key of the mint account to be initialized which is required to be writable, because the token’s data will be stored in themint
account during initialization.
mint_authority_pubkey
: The public key of the mint authority, which will have the ability to mint new tokens.
freeze_authority_pubkey
: An optional public key for the freeze authority, which will have the ability to freeze token accounts associated with this mint.
decimals
: The number of decimal places used by the token (i.e., the token's precision).
/// ---token/program/src/instruction.rs--- /// Creates a `InitializeMint2` instruction. pub fn initialize_mint2( token_program_id: &Pubkey, mint_pubkey: &Pubkey, mint_authority_pubkey: &Pubkey, freeze_authority_pubkey: Option<&Pubkey>, decimals: u8, ) -> Result<Instruction, ProgramError> { check_program_account(token_program_id)?; let freeze_authority = freeze_authority_pubkey.cloned().into(); let data = TokenInstruction::InitializeMint2 { mint_authority: *mint_authority_pubkey, freeze_authority, decimals, } .pack(); let accounts = vec![AccountMeta::new(*mint_pubkey, false)]; Ok(Instruction { program_id: *token_program_id, accounts, data, }) }
Token Program Entrypoint
The
process_instruction
function in the Token Program serves as the entry point for processing instructions sent to the Token Program. This function is automatically invoked by the Solana runtime whenever a transaction targeting the Token Program is executed. Parameters
program_id
: A reference to thePubkey
that represents the ID of the Token Program. This helps ensure that the instruction is being processed by the correct program.
accounts
: A slice ofAccountInfo
objects. EachAccountInfo
provides information about an account involved in the instruction, including its public key, lamport balance, data, owner program, and other metadata.
instruction_data
: A slice of bytes that represents the serialized instruction data. This data will be deserialized and processed by the Token Program's processor.
process_instruction
calls Processor::process
to deserialize the instruction data, and find the right function to execute the instruction./// ---token/program/src/entrypoint.rs--- solana_program::entrypoint!(process_instruction); fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { if let Err(error) = Processor::process(program_id, accounts, instruction_data) { // catch the error so we can print it error.print::<TokenError>(); return Err(error); } Ok(()) }
process
basically unpacks the serialized instruction data to get enum instruction
, and find the correct function to execute it./// ---token/program/src/processor.rs--- /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenInstruction::unpack(input)?; match instruction { TokenInstruction::InitializeMint { decimals, mint_authority, freeze_authority, } => { msg!("Instruction: InitializeMint"); Self::process_initialize_mint(accounts, decimals, mint_authority, freeze_authority) } TokenInstruction::InitializeMint2 { decimals, mint_authority, freeze_authority, } => { msg!("Instruction: InitializeMint2"); Self::process_initialize_mint2(accounts, decimals, mint_authority, freeze_authority) } } // ... }
process_initialize_mint2
process_initialize_mint2
function in the Token Program is responsible for initializing a new token mint. This function is a wrapper that calls the internal _process_initialize_mint
function, which contains the actual logic for mint initialization. The difference between
process_initialize_mint
and process_initialize_mint2
the rent_sysvar_account
parameter indicates whether the transaction context has Rent
account./// ---token/program/src/processor.rs--- /// Processes an [InitializeMint](enum.TokenInstruction.html) instruction. pub fn process_initialize_mint( accounts: &[AccountInfo], decimals: u8, mint_authority: Pubkey, freeze_authority: COption<Pubkey>, ) -> ProgramResult { Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, true) } /// Processes an [InitializeMint2](enum.TokenInstruction.html) instruction. pub fn process_initialize_mint2( accounts: &[AccountInfo], decimals: u8, mint_authority: Pubkey, freeze_authority: COption<Pubkey>, ) -> ProgramResult { Self::_process_initialize_mint(accounts, decimals, mint_authority, freeze_authority, false) }
_process_initialize_mint
This function performs the actual initialization of the mint.
Parameters
accounts
: A slice ofAccountInfo
objects.
decimals
: The number of decimal places for the token.
mint_authority
: The public key of the mint authority.
freeze_authority
: An optional public key for the freeze authority.
rent_sysvar_account
: A boolean indicating whether the transaction context hasRent
account.
Steps
- Account Information Iteration:
Iterates over the provided accounts to extract relevant account information.
- Get Rent instance:
Get the
Rent
instance which is used to calculate rent. If rent_sysvar_account
is true, it gets the rent from the next account; otherwise, it uses the default rent.- Unpack and Check Mint Account:
Unpacks the mint account data. Verify the mint is not initialized, it returns an
AlreadyInUse
error.- Rent Exemption Check:
Checks if the mint account has enough lamports to be rent-exempt. If not, it returns a
NotRentExempt
error.- Initialize Mint Account:
Sets the mint authority, decimal precision, and freeze authority. Marks the mint as initialized.
- Save Mint Data:
Packs the mint data back into the mint account.
- Return OK Result.
Note it’s the Token Program which modifies the data in the
mint
, that’s why in the first instruction its necessary to set the Token Program as the owner of the mint
. (In solana, only account owner can modify account’s data)./// ---token/program/src/processor.rs--- fn _process_initialize_mint( accounts: &[AccountInfo], decimals: u8, mint_authority: Pubkey, freeze_authority: COption<Pubkey>, rent_sysvar_account: bool, ) -> ProgramResult { // Account Information Iteration: let account_info_iter = &mut accounts.iter(); let mint_info = next_account_info(account_info_iter)?; let mint_data_len = mint_info.data_len(); // Get Rent instance: let rent = if rent_sysvar_account { Rent::from_account_info(next_account_info(account_info_iter)?)? } else { Rent::get()? }; // Unpack and Check Mint Account: let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow())?; if mint.is_initialized { return Err(TokenError::AlreadyInUse.into()); } // Rent Exemption Check: if !rent.is_exempt(mint_info.lamports(), mint_data_len) { return Err(TokenError::NotRentExempt.into()); } // Initialize Mint Account: mint.mint_authority = COption::Some(mint_authority); mint.decimals = decimals; mint.is_initialized = true; mint.freeze_authority = freeze_authority; // Pack and Save Mint Data: Mint::pack(mint, &mut mint_info.data.borrow_mut())?; // Return OK Result. Ok(()) }
In fact, we can see there is
supply
field in the Mint account struct. But in the mint
account initialization process, Token Program just keeps it as default 0./// ---token/program/src/state.rs--- #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] 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>, }
Initialize Associated Token Account
To improve parallel tranasction execution, SPL token standard stores each account’s token information in separate PDA data account. This data account is called Token Account.
Associated Token Account(ATA) is one type of Token Account which has a deterministic Token Account address generation algorithm. Use ATA standard, we can easily got corresponding token account of certain mint account of certain wallet.
Token Account Definition in SPL
/// ---token/program/src/state.rs--- /// Account state. #[repr(u8)] #[derive(Clone, Copy, Debug, Default, PartialEq, TryFromPrimitive)] pub enum AccountState { /// Account is not yet initialized #[default] Uninitialized, /// Account is initialized; the account owner and/or delegate may perform /// permitted operations on this account Initialized, /// Account has been frozen by the mint freeze authority. Neither the /// account owner nor the delegate are able to perform operations on /// this account. Frozen, } #[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq)] 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>, }
ATA address calculation
solana/spl-token:getAssociatedTokenAddress
implements ATA address calculation.We can see that ATA’s calculated based on:
- Associated Token Program ID.
- Token Program ID.
- mint’s public key.
- owner’s public key.
- bump.
// ---@solana/spl-token/src/state/mint.ts--- /** * Async version of getAssociatedTokenAddressSync * For backwards compatibility * * @param mint Token mint account * @param owner Owner of the new account * @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address) * @param programId SPL Token program account * @param associatedTokenProgramId SPL Associated Token program account * * @return Promise containing the address of the associated token account */ export async function getAssociatedTokenAddress( mint: PublicKey, owner: PublicKey, allowOwnerOffCurve = false, programId = TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID ): Promise<PublicKey> { if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) throw new TokenOwnerOffCurveError(); const [address] = await PublicKey.findProgramAddress( [owner.toBuffer(), programId.toBuffer(), mint.toBuffer()], associatedTokenProgramId ); return address; }
In the implementation of solana/spl-token library, we can see the construction of ATA creation instruction. Only payer should be the signer who is to pay the lamport to create and initialize the ATA. And ATA should be writable where token’s ownership data will be recorded.
One interesting thing is that the instruction data is empty. The ATA program will check the instruction, if it’s empty, it will regard this as an ATA creation instruction.
/// ---/solana/spl-token/src/instructions/associatedTokenAccount.ts--- /** * Construct a CreateAssociatedTokenAccount instruction * * @param payer Payer of the initialization fees * @param associatedToken New associated token account * @param owner Owner of the new account * @param mint Token mint account * @param programId SPL Token program account * @param associatedTokenProgramId SPL Associated Token program account * * @return Instruction to add to a transaction */ export function createAssociatedTokenAccountInstruction( payer: PublicKey, associatedToken: PublicKey, owner: PublicKey, mint: PublicKey, programId = TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID ): TransactionInstruction { return buildAssociatedTokenAccountInstruction( payer, associatedToken, owner, mint, Buffer.alloc(0), programId, associatedTokenProgramId ); } function buildAssociatedTokenAccountInstruction( payer: PublicKey, associatedToken: PublicKey, owner: PublicKey, mint: PublicKey, instructionData: Buffer, programId = TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID ): TransactionInstruction { const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: associatedToken, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: false, isWritable: false }, { pubkey: mint, isSigner: false, isWritable: false }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, { pubkey: programId, isSigner: false, isWritable: false }, ]; return new TransactionInstruction({ keys, programId: associatedTokenProgramId, data: instructionData, }); }
ATA program also exposes instruction construction interface.
/// Creates Create instruction pub fn create_associated_token_account( funding_address: &Pubkey, wallet_address: &Pubkey, token_mint_address: &Pubkey, token_program_id: &Pubkey, ) -> Instruction { build_associated_token_account_instruction( funding_address, wallet_address, token_mint_address, token_program_id, AssociatedTokenAccountInstruction::Create, ) } fn build_associated_token_account_instruction( funding_address: &Pubkey, wallet_address: &Pubkey, token_mint_address: &Pubkey, token_program_id: &Pubkey, instruction: AssociatedTokenAccountInstruction, ) -> Instruction { let associated_account_address = get_associated_token_address_with_program_id( wallet_address, token_mint_address, token_program_id, ); // safety check, assert if not a creation instruction assert_matches!( instruction, AssociatedTokenAccountInstruction::Create | AssociatedTokenAccountInstruction::CreateIdempotent ); Instruction { program_id: id(), accounts: vec![ AccountMeta::new(*funding_address, true), AccountMeta::new(associated_account_address, false), AccountMeta::new_readonly(*wallet_address, false), AccountMeta::new_readonly(*token_mint_address, false), AccountMeta::new_readonly(solana_program::system_program::id(), false), AccountMeta::new_readonly(*token_program_id, false), ], data: borsh::to_vec(&instruction).unwrap(), } }
ATA address computation in ATA program:
/// ---associated-token-account/program/src/lib.rs--- /// Derives the associated token account address for the given wallet address, /// token mint and token program id pub fn get_associated_token_address_with_program_id( wallet_address: &Pubkey, token_mint_address: &Pubkey, token_program_id: &Pubkey, ) -> Pubkey { get_associated_token_address_and_bump_seed( wallet_address, token_mint_address, &id(), token_program_id, ) .0 } pub(crate) fn get_associated_token_address_and_bump_seed( wallet_address: &Pubkey, token_mint_address: &Pubkey, program_id: &Pubkey, token_program_id: &Pubkey, ) -> (Pubkey, u8) { get_associated_token_address_and_bump_seed_internal( wallet_address, token_mint_address, program_id, token_program_id, ) } fn get_associated_token_address_and_bump_seed_internal( wallet_address: &Pubkey, token_mint_address: &Pubkey, program_id: &Pubkey, token_program_id: &Pubkey, ) -> (Pubkey, u8) { Pubkey::find_program_address( &[ &wallet_address.to_bytes(), &token_program_id.to_bytes(), &token_mint_address.to_bytes(), ], program_id, ) }
ATA creation
process_instruction
unpacks instruction and executes process_create_associated_token_account
to create ATA./// ---associated-token-account/program/src/processor.rs--- /// Instruction processor pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8], ) -> ProgramResult { let instruction = if input.is_empty() { AssociatedTokenAccountInstruction::Create } else { AssociatedTokenAccountInstruction::try_from_slice(input) .map_err(|_| ProgramError::InvalidInstructionData)? }; msg!("{:?}", instruction); match instruction { AssociatedTokenAccountInstruction::Create => { process_create_associated_token_account(program_id, accounts, CreateMode::Always) } AssociatedTokenAccountInstruction::CreateIdempotent => { process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent) } AssociatedTokenAccountInstruction::RecoverNested => { process_recover_nested(program_id, accounts) } } }
process_create_associated_token_account
sets up an associated token account (ATA) for a given wallet and token mint, ensuring the account is properly initialized and associated with the correct entities. Parameters
program_id
: The public key of the associated token account program.
accounts
: A slice ofAccountInfo
objects representing the accounts involved in the instruction.
create_mode
: Specifies the mode of creation, which could beAlways
: Always try to create the ATAIdempotent
: Only try to create the ATA if non-existent.
Steps
- Iterate Over Account Info:
iterates over the provided accounts to extract relevant account information.
- Derive Associated Token Address And Check Consistency:
derives the associated token address and bump seed using the wallet address, token mint address, program ID, and token program ID. Checks whether the derived ATA address is the same as the one provided in the accounts.
- Idempotent Mode Check:
If the creation mode is
Idempotent
, the function checks if the associated token account already exists and is correctly configured. If it is, the function exits early without making any changes.- Check Owner(Always Mode):
The function checks that the owner of the associated token account is the system program. If not, it returns an error.
- Retrieve Rent instance(used to calculate rent).
- Construct Signer Seeds of ATA PDA.
prepares the seeds required to sign the creation of the associated token account, using the wallet address, token program ID, token mint address, and bump seed
- Calculate Account Length:
calculates the required length for the associated token account, including any necessary extensions. Account data length is used to calculate rent to exempt account.
- Create PDA Account:
creates the associated token account using the calculated rent, length, and signer seeds.
- Initialize Associated Token Account:
initializes the associated token account by invoking the
initialize_immutable_owner
and initialize_account3
instructions from the SPL Token Program./// ---associated-token-account/program/src/processor.rs--- /// Specify when to create the associated token account #[derive(PartialEq)] enum CreateMode { /// Always try to create the ATA Always, /// Only try to create the ATA if non-existent Idempotent, } /// Processes CreateAssociatedTokenAccount instruction fn process_create_associated_token_account( program_id: &Pubkey, accounts: &[AccountInfo], create_mode: CreateMode, ) -> ProgramResult { // Iterate Over Account Info: let account_info_iter = &mut accounts.iter(); let funder_info = next_account_info(account_info_iter)?; let associated_token_account_info = next_account_info(account_info_iter)?; let wallet_account_info = next_account_info(account_info_iter)?; let spl_token_mint_info = next_account_info(account_info_iter)?; let system_program_info = next_account_info(account_info_iter)?; let spl_token_program_info = next_account_info(account_info_iter)?; let spl_token_program_id = spl_token_program_info.key; // Derive Associated Token Address And Check Consistency: let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal( wallet_account_info.key, spl_token_mint_info.key, program_id, spl_token_program_id, ); if associated_token_address != *associated_token_account_info.key { msg!("Error: Associated address does not match seed derivation"); return Err(ProgramError::InvalidSeeds); } // Idempotent Mode Check: if create_mode == CreateMode::Idempotent && associated_token_account_info.owner == spl_token_program_id { let ata_data = associated_token_account_info.data.borrow(); if let Ok(associated_token_account) = StateWithExtensions::<Account>::unpack(&ata_data) { // Check owner matches if associated_token_account.base.owner != *wallet_account_info.key { let error = AssociatedTokenAccountError::InvalidOwner; msg!("{}", error); return Err(error.into()); } // check the mint account matches if associated_token_account.base.mint != *spl_token_mint_info.key { return Err(ProgramError::InvalidAccountData); } return Ok(()); } } // Check the ATA hasn't been created (owned by system program) if *associated_token_account_info.owner != system_program::id() { return Err(ProgramError::IllegalOwner); } // Retrieve Rent instance to calculate rent to exempt ATA let rent = Rent::get()?; // Construct Signer Seeds of ATA PDA. let associated_token_account_signer_seeds: &[&[_]] = &[ &wallet_account_info.key.to_bytes(), &spl_token_program_id.to_bytes(), &spl_token_mint_info.key.to_bytes(), &[bump_seed], ]; // Calculate Account Length: let account_len = get_account_len( spl_token_mint_info, spl_token_program_info, &[ExtensionType::ImmutableOwner], )?; // Create PDA Account: create_pda_account( funder_info, &rent, account_len, spl_token_program_id, system_program_info, associated_token_account_info, associated_token_account_signer_seeds, )?; // Initialize Associated Token Account: msg!("Initialize the associated token account"); invoke( &spl_token_2022::instruction::initialize_immutable_owner( spl_token_program_id, associated_token_account_info.key, )?, &[ associated_token_account_info.clone(), spl_token_program_info.clone(), ], )?; invoke( &spl_token_2022::instruction::initialize_account3( spl_token_program_id, associated_token_account_info.key, spl_token_mint_info.key, wallet_account_info.key, )?, &[ associated_token_account_info.clone(), spl_token_mint_info.clone(), wallet_account_info.clone(), spl_token_program_info.clone(), ], ) }
ATA account address calculation
get_associated_token_address_and_bump_seed_internal
calculates PDA ATA address according:-
wallet_address
: token owner address
token_program_id
: spl token program or spl token program 2022
token_mint_address
:mint
account address
program_id
: ATA program address
From the address calculation, we can see that the Assoicated Token program is the parent account of ATA account.
/// ---/associated-token-account/program/src/lib.rs--- fn get_associated_token_address_and_bump_seed_internal( wallet_address: &Pubkey, token_mint_address: &Pubkey, program_id: &Pubkey, token_program_id: &Pubkey, ) -> (Pubkey, u8) { Pubkey::find_program_address( &[ &wallet_address.to_bytes(), &token_program_id.to_bytes(), &token_mint_address.to_bytes(), ], program_id, ) }
create pda account
create_pda_account
handles operation of ATA creation where ATA is assigned to spl Token Program(or Token Program 2022).Parameters:
payer
: The account that will pay for the creation of the PDA account.
rent
: The Rent instance used to calculate rent exempt lamport amount.
space
: The amount of space (in bytes) to allocate for the new PDA account.
owner
: The public key of the owner program for the new PDA account.
system_program
: The system program account.
new_pda_account
: The account information for the new PDA.
new_pda_signer_seeds
: The seeds used to derive the PDA.
Steps:
- Check and Fund PDA Account:
If the
new_pda_account
already has some lamports (balance greater than 0), it calculates the additional lamports needed to meet the minimum balance required by the rent. If additional lamports are required, it invokes a transfer instruction to fund the new_pda_account
.- Allocate Space for PDA:
It uses the
invoke_signed
function to allocate space for the new_pda_account
using the allocate
system instruction. - Assign Owner to PDA:
It uses the
invoke_signed
function to assign the owner program to the new_pda_account
using the assign
system instruction.- Create New PDA Account:
If the
new_pda_account
has no lamports (balance is 0), it directly creates the account using the create_account
system instruction.Note that the minimal lamports of the to-be-created account is 1. This is because balance of zero would make the account eligible for deletion during rent collection. Ensuring a minimum balance of at least 1 lamport guarantees that the account is considered active and is not removed by the Solana runtime.
/// Creates associated token account using Program Derived Address for the given /// seeds pub fn create_pda_account<'a>( payer: &AccountInfo<'a>, rent: &Rent, space: usize, owner: &Pubkey, system_program: &AccountInfo<'a>, new_pda_account: &AccountInfo<'a>, new_pda_signer_seeds: &[&[u8]], ) -> ProgramResult { if new_pda_account.lamports() > 0 { let required_lamports = rent .minimum_balance(space) .max(1) .saturating_sub(new_pda_account.lamports()); if required_lamports > 0 { invoke( &system_instruction::transfer(payer.key, new_pda_account.key, required_lamports), &[ payer.clone(), new_pda_account.clone(), system_program.clone(), ], )?; } invoke_signed( &system_instruction::allocate(new_pda_account.key, space as u64), &[new_pda_account.clone(), system_program.clone()], &[new_pda_signer_seeds], )?; invoke_signed( &system_instruction::assign(new_pda_account.key, owner), &[new_pda_account.clone(), system_program.clone()], &[new_pda_signer_seeds], ) } else { invoke_signed( &system_instruction::create_account( payer.key, new_pda_account.key, rent.minimum_balance(space).max(1), space as u64, owner, ), &[ payer.clone(), new_pda_account.clone(), system_program.clone(), ], &[new_pda_signer_seeds], ) } }
When there is some lamports in the ATA account, it doesn’t call
create_account
directly but calls transfer
allocate
and assign
system instructions instead. This is because create_account
system instruction requires the account to be created has zero lamport./// ---programs/system/src/system_processor.rs--- #[allow(clippy::too_many_arguments)] fn create_account( from_account_index: IndexOfAccount, to_account_index: IndexOfAccount, to_address: &Address, lamports: u64, space: u64, owner: &Pubkey, signers: &HashSet<Pubkey>, invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, ) -> Result<(), InstructionError> { // if it looks like the `to` account is already in use, bail { let mut to = instruction_context .try_borrow_instruction_account(transaction_context, to_account_index)?; if to.get_lamports() > 0 { ic_msg!( invoke_context, "Create Account: account {:?} already in use", to_address ); return Err(SystemError::AccountAlreadyInUse.into()); } allocate_and_assign(&mut to, to_address, space, owner, signers, invoke_context)?; } // ... }
Initialize token account
In the
process_create_associated_token_account
, after the ATA has been created, it initializes it by invoking initialize_immutable_owner
and initialize_account3
./// ---associated-token-account/program/src/processor.rs--- /// Processes CreateAssociatedTokenAccount instruction fn process_create_associated_token_account( program_id: &Pubkey, accounts: &[AccountInfo], create_mode: CreateMode, ) -> ProgramResult { // ... msg!("Initialize the associated token account"); invoke( &spl_token_2022::instruction::initialize_immutable_owner( spl_token_program_id, associated_token_account_info.key, )?, &[ associated_token_account_info.clone(), spl_token_program_info.clone(), ], )?; invoke( &spl_token_2022::instruction::initialize_account3( spl_token_program_id, associated_token_account_info.key, spl_token_mint_info.key, wallet_account_info.key, )?, &[ associated_token_account_info.clone(), spl_token_mint_info.clone(), wallet_account_info.clone(), spl_token_program_info.clone(), ], ) }
initialize_immutable_owner
Token Program doesn’t support immutable owner extension.
/// ---token/program/src/processor.rs--- /// Processes an [InitializeImmutableOwner](enum.TokenInstruction.html) /// instruction pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let token_account_info = next_account_info(account_info_iter)?; let account = Account::unpack_unchecked(&token_account_info.data.borrow())?; if account.is_initialized() { return Err(TokenError::AlreadyInUse.into()); } msg!("Please upgrade to SPL Token 2022 for immutable owner support"); Ok(()) }
Token Program 2022 supports immutable owner extension.
It does some sanity on the token account, and wirte the
immutableOwner
extension to the account data’s TLV part./// ---token/program-2022/src/processor.rs--- /// Processes an [InitializeImmutableOwner](enum.TokenInstruction.html) /// instruction pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let token_account_info = next_account_info(account_info_iter)?; let token_account_data = &mut token_account_info.data.borrow_mut(); let mut token_account = PodStateWithExtensionsMut::<PodAccount>::unpack_uninitialized(token_account_data)?; token_account .init_extension::<ImmutableOwner>(true) .map(|_| ()) }
initialize_account3
initialize_account3
is the entry to initialize a token account. It delegates the actual initialization work to the _process_initialize_account
function./// ---token/program-2022/src/processor.rs--- /// Processes an [InitializeAccount3](enum.TokenInstruction.html) /// instruction. pub fn process_initialize_account3(accounts: &[AccountInfo], owner: &Pubkey) -> ProgramResult { Self::_process_initialize_account(accounts, Some(owner), false) }
_process_initialize_account
performs the detailed work of initializing a new token account. It validates the account and mint, sets up necessary extensions, and initializes the account's state.Parameters
accounts
: A slice of relatedAccountInfo
objects.
owner
: An optional reference to thePubkey
of the account owner.
rent_sysvar_account
: A boolean indicating whether instruction context includes rent sysvar account for rent calculation.
Steps:
- Account Iteration
Iterate over the provided accounts to extract relevant account information.
- Get Rent Configuration
Get the rent account used to calculate rent exempt lamports amount.
- Unpack Uninitialized Account’s Data
Unpack the account data for the new account and ensure it is uninitialized.
- Rent Exemption Check
Verify that the account is rent-exempt.
- Deserialize mint account data
- Check whether the
mint
account hasPermanentDelegate
extension
If it has, then emits message to notify.
- Initialize extensions of token account required by the
mint
account
Decode required token account extensions from
mint
account, initialize them on the token account.- Set Initial Account State
Set the initial state of the account based on the mint's default account state or initialize it to
AccountState::Initialized
.- Initialize Account Fields
Initialize various fields of the account, such as mint, owner, delegate, and state.
- Handle Native Account
If the account is a native SOL account, set the
is_native
flag and calculate the rent-exempt reserve. Otherwise, initialize the account amount to zero.- Initialize Account Type
Initialize the account type to be
Account
. Other account types include Uninitialized
and mint
./// ---token/program-2022/src/processor.rs--- fn _process_initialize_account( accounts: &[AccountInfo], owner: Option<&Pubkey>, rent_sysvar_account: bool, ) -> ProgramResult { /// Account Iteration let account_info_iter = &mut accounts.iter(); let new_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; let owner = if let Some(owner) = owner { owner } else { next_account_info(account_info_iter)?.key }; let new_account_info_data_len = new_account_info.data_len(); /// Get Rent Configuration let rent = if rent_sysvar_account { Rent::from_account_info(next_account_info(account_info_iter)?)? } else { Rent::get()? }; /// Unpack Uninitialized Account’s Data let mut account_data = new_account_info.data.borrow_mut(); // unpack_uninitialized checks account.base.is_initialized() under the hood let mut account = PodStateWithExtensionsMut::<PodAccount>::unpack_uninitialized(&mut account_data)?; /// Rent Exemption Check: if !rent.is_exempt(new_account_info.lamports(), new_account_info_data_len) { return Err(TokenError::NotRentExempt.into()); } /// Deserialize mint account data // get_required_account_extensions checks mint validity let mint_data = mint_info.data.borrow(); let mint = PodStateWithExtensions::<PodMint>::unpack(&mint_data) .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?; /// Check whether the mint account has PermanentDelegate extension if mint .get_extension::<PermanentDelegate>() .map(|e| Option::<Pubkey>::from(e.delegate).is_some()) .unwrap_or(false) { msg!("Warning: Mint has a permanent delegate, so tokens in this account may be seized at any time"); } /// Initialize extensions of token account required by the mint account let required_extensions = Self::get_required_account_extensions_from_unpacked_mint(mint_info.owner, &mint)?; if ExtensionType::try_calculate_account_len::<Account>(&required_extensions)? > new_account_info_data_len { return Err(ProgramError::InvalidAccountData); } for extension in required_extensions { account.init_account_extension_from_type(extension)?; } /// Set Initial Account State let starting_state = if let Ok(default_account_state) = mint.get_extension::<DefaultAccountState>() { AccountState::try_from(default_account_state.state) .or(Err(ProgramError::InvalidAccountData))? } else { AccountState::Initialized }; /// Initialize Account Fields account.base.mint = *mint_info.key; account.base.owner = *owner; account.base.close_authority = PodCOption::none(); account.base.delegate = PodCOption::none(); account.base.delegated_amount = 0.into(); account.base.state = starting_state.into(); /// Handle Native Account if cmp_pubkeys(mint_info.key, &native_mint::id()) { let rent_exempt_reserve = rent.minimum_balance(new_account_info_data_len); account.base.is_native = PodCOption::some(rent_exempt_reserve.into()); account.base.amount = new_account_info .lamports() .checked_sub(rent_exempt_reserve) .ok_or(TokenError::Overflow)? .into(); } else { account.base.is_native = PodCOption::none(); account.base.amount = 0.into(); }; /// Initialize Account Type account.init_account_type()?; Ok(()) }
Mint Token
Owner of
mint
account can mint tokens to token account.token program 2022
mint_to
functions constructs mint operation’s instruction. We can see that mint operation doesn’t require the mint and token account to be signer, but require the owner of the mint account to be signer, if the mint account’s owner is multi-sig, then corresponding wallets should be signers./// ---token/program-2022/src/instruction.rs--- /// Creates a `MintTo` instruction. pub fn mint_to( token_program_id: &Pubkey, mint_pubkey: &Pubkey, account_pubkey: &Pubkey, owner_pubkey: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, ) -> Result<Instruction, ProgramError> { check_spl_token_program_account(token_program_id)?; let data = TokenInstruction::MintTo { amount }.pack(); let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); accounts.push(AccountMeta::new(*mint_pubkey, false)); accounts.push(AccountMeta::new(*account_pubkey, false)); accounts.push(AccountMeta::new_readonly( *owner_pubkey, signer_pubkeys.is_empty(), )); for signer_pubkey in signer_pubkeys.iter() { accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); } Ok(Instruction { program_id: *token_program_id, accounts, data, }) }
processor::process_mint_to
performs mint operation details.Steps:
- Account Iteration and Extraction:
Iterates over the provided accounts to extract relevant account information.
- Unpack and Validate Destination Account:
Unpacks the destination account data and checks if the account is frozen or a native SOL account. If either condition is true, it returns an error. And checks the destination token account’s mint account is the mint account in the instruction context.
- Unpack Mint Account:
Unpacks the mint account data.
- Non-Transferable Mint Check:
If the mint is non-transferable, it ensures that the destination account has immutable ownership. If not, it returns an error. This makes sense, because even if the mint account itself requires token is non-transferable, users can still transfer token by transfering owner of token account.
- Decimals Check:
If expected_decimals is provided, it checks if the mint's decimals match the expected decimals. If not, it returns an error.
- Validate Mint Authority:
Validates the mint authority.
If there is mint authority, then validate signers. If there is no mint authority which means the supply is fixed, then return error.
- Check Program Accounts:
Checks if the mint and destination accounts are owned by the correct program.
- Mint Tokens:
Mints the specified amount of tokens to the destination account and updates the mint's supply.
- Return Success.
/// ---token/program-2022/src/processor.rs--- /// Processes a [MintTo](enum.TokenInstruction.html) instruction. pub fn process_mint_to( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, expected_decimals: Option<u8>, ) -> ProgramResult { // Account Iteration and Extraction: let account_info_iter = &mut accounts.iter(); let mint_info = next_account_info(account_info_iter)?; let destination_account_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; let owner_info_data_len = owner_info.data_len(); // Unpack and Validate Destination Account: let mut destination_account_data = destination_account_info.data.borrow_mut(); let destination_account = PodStateWithExtensionsMut::<PodAccount>::unpack(&mut destination_account_data)?; if destination_account.base.is_frozen() { return Err(TokenError::AccountFrozen.into()); } if destination_account.base.is_native() { return Err(TokenError::NativeNotSupported.into()); } if !cmp_pubkeys(mint_info.key, &destination_account.base.mint) { return Err(TokenError::MintMismatch.into()); } // Unpack Mint Account: let mut mint_data = mint_info.data.borrow_mut(); let mint = PodStateWithExtensionsMut::<PodMint>::unpack(&mut mint_data)?; // Non-Transferable Mint Check: // If the mint if non-transferable, only allow minting to accounts // with immutable ownership. if mint.get_extension::<NonTransferable>().is_ok() && destination_account .get_extension::<ImmutableOwner>() .is_err() { return Err(TokenError::NonTransferableNeedsImmutableOwnership.into()); } // Decimals Check: if let Some(expected_decimals) = expected_decimals { if expected_decimals != mint.base.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } } // Validate Mint Authority: match &mint.base.mint_authority { PodCOption { option: PodCOption::<Pubkey>::SOME, value: mint_authority, } => Self::validate_owner( program_id, mint_authority, owner_info, owner_info_data_len, account_info_iter.as_slice(), )?, _ => return Err(TokenError::FixedSupply.into()), } // Check Program Accounts: // Revisit this later to see if it's worth adding a check to reduce // compute costs, ie: // if amount == 0 check_program_account(mint_info.owner)?; check_program_account(destination_account_info.owner)?; // Mint Tokens: destination_account.base.amount = u64::from(destination_account.base.amount) .checked_add(amount) .ok_or(TokenError::Overflow)? .into(); mint.base.supply = u64::from(mint.base.supply) .checked_add(amount) .ok_or(TokenError::Overflow)? .into(); // Return Success. Ok(()) }
Transfer Token
instruction::transfer
helps construct transfer operation’s instruction.We can see the only signer is the owner of the token account.
/// ---token/program-2022/src/instruction.rs--- /// Creates a `Transfer` instruction. #[deprecated( since = "4.0.0", note = "please use `transfer_checked` or `transfer_checked_with_fee` instead" )] pub fn transfer( token_program_id: &Pubkey, source_pubkey: &Pubkey, destination_pubkey: &Pubkey, authority_pubkey: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, ) -> Result<Instruction, ProgramError> { check_spl_token_program_account(token_program_id)?; #[allow(deprecated)] let data = TokenInstruction::Transfer { amount }.pack(); let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); accounts.push(AccountMeta::new(*source_pubkey, false)); accounts.push(AccountMeta::new(*destination_pubkey, false)); accounts.push(AccountMeta::new_readonly( *authority_pubkey, signer_pubkeys.is_empty(), )); for signer_pubkey in signer_pubkeys.iter() { accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); } Ok(Instruction { program_id: *token_program_id, accounts, data, }) }
The
transfer
logic in Token Program 2022 is quite complicated due to the need to handle many potential extensions and hook. We can use Token Program’s transfer logic to analyze the overall process.The basic logic is simple: validate signer and update token amount. If the authority account is the delegate set in the token account, the delegated amount will be substracted.
Parameters
program_id
: The public key of the program invoked.
accounts
: A slice ofAccountInfo
objects representing the accounts involved in the transfer.
amount
: The amount of tokens to transfer.
expected_decimals
: An optional parameter to check if the mint's decimals match the expected decimals.
Steps
- Account Iteration and Extraction:
Iterate over the provided accounts to extract relevant account information.
- Unpack Source and Destination Accounts:
Unpack the account data for the source and destination accounts.
- Frozen Account Check:
Check if either the source or destination account is frozen. If so, return an error.
- Sufficient Funds Check:
Ensure the source account has enough tokens to transfer the specified amount. If not, return an error.
- Mint Mismatch Check:
Check if the mints of the source and destination accounts match. If not, return an error.
- Expected Decimals Check:
If expected_decimals is provided, check if the mint's decimals match the expected decimals. If not, return an error.
- Self Transfer Check:
Check if the transfer is a self-transfer (i.e., source and destination accounts are the same).
- Delegate and Owner Validation:
Validate the transfer authority based on whether the source account has a delegate set.
- Check Account Owners:
For self-transfers or zero-amount transfers, check that the program owns both the source and destination accounts.
- Return Early for Self Transfers:
- Update Account Balances:
Update the balances of the source and destination accounts.
- Handle Native Token Accounts:
If the source account is a native SOL account, update the lamport balances.
- Pack Updated Accounts:
Pack the updated source and destination account data back into their respective accounts.
- Return Success.
For Token Account holds native sol, we don’t need to worry that the lamport will be less then rent-exempt lamport amount, because the
account.amount
excludes the reserved rent-exempt lamport (set to be 0) during the token account initialization. Reserved lamport won’t be transferred.Also we can see that the account owner check only happens when its self-transfer or transfer amount equals 0, I think this is to save computation unit. Those two situation has no operation on account. For other cases, if the owner of those two accounts is not the corresponding token program, the runtime will throw error automatically.
/// ---token/program/src/processor.rs--- /// Processes a [Transfer](enum.TokenInstruction.html) instruction. pub fn process_transfer( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, expected_decimals: Option<u8>, ) -> ProgramResult { // Account Iteration and Extraction: let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let expected_mint_info = if let Some(expected_decimals) = expected_decimals { Some((next_account_info(account_info_iter)?, expected_decimals)) } else { None }; let destination_account_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; // Unpack Source and Destination Accounts: let mut source_account = Account::unpack(&source_account_info.data.borrow())?; let mut destination_account = Account::unpack(&destination_account_info.data.borrow())?; /// Frozen Account Check: if source_account.is_frozen() || destination_account.is_frozen() { return Err(TokenError::AccountFrozen.into()); } /// Sufficient Funds Check: if source_account.amount < amount { return Err(TokenError::InsufficientFunds.into()); } // Mint Mismatch Check: if !Self::cmp_pubkeys(&source_account.mint, &destination_account.mint) { return Err(TokenError::MintMismatch.into()); } // Expected Decimals Check: if let Some((mint_info, expected_decimals)) = expected_mint_info { if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { return Err(TokenError::MintMismatch.into()); } let mint = Mint::unpack(&mint_info.data.borrow_mut())?; if expected_decimals != mint.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } } // Self Transfer Check: let self_transfer = Self::cmp_pubkeys(source_account_info.key, destination_account_info.key); // Delegate and Owner Validation: match source_account.delegate { COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { Self::validate_owner( program_id, delegate, authority_info, account_info_iter.as_slice(), )?; if source_account.delegated_amount < amount { return Err(TokenError::InsufficientFunds.into()); } if !self_transfer { source_account.delegated_amount = source_account .delegated_amount .checked_sub(amount) .ok_or(TokenError::Overflow)?; if source_account.delegated_amount == 0 { source_account.delegate = COption::None; } } } _ => Self::validate_owner( program_id, &source_account.owner, authority_info, account_info_iter.as_slice(), )?, }; // Check Account Owners: if self_transfer || amount == 0 { Self::check_account_owner(program_id, source_account_info)?; Self::check_account_owner(program_id, destination_account_info)?; } // Return Early for Self Transfers: // This check MUST occur just before the amounts are manipulated // to ensure self-transfers are fully validated if self_transfer { return Ok(()); } // Update Account Balances: source_account.amount = source_account .amount .checked_sub(amount) .ok_or(TokenError::Overflow)?; destination_account.amount = destination_account .amount .checked_add(amount) .ok_or(TokenError::Overflow)?; // Handle Native Token Accounts: if source_account.is_native() { let source_starting_lamports = source_account_info.lamports(); **source_account_info.lamports.borrow_mut() = source_starting_lamports .checked_sub(amount) .ok_or(TokenError::Overflow)?; let destination_starting_lamports = destination_account_info.lamports(); **destination_account_info.lamports.borrow_mut() = destination_starting_lamports .checked_add(amount) .ok_or(TokenError::Overflow)?; } // Pack Updated Accounts: Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; Account::pack( destination_account, &mut destination_account_info.data.borrow_mut(), )?; Ok(()) }
Approve Token
approve
operation is similar to ERC20’s. It approves the certain amount of token’s transfer authority to another address.From the instruction construction, we can see that it allows the owner of the token account to sign the transaction.
/// ---token/program/src/instruction.rs--- /// Creates an `Approve` instruction. pub fn approve( token_program_id: &Pubkey, source_pubkey: &Pubkey, delegate_pubkey: &Pubkey, owner_pubkey: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, ) -> Result<Instruction, ProgramError> { check_program_account(token_program_id)?; let data = TokenInstruction::Approve { amount }.pack(); let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); accounts.push(AccountMeta::new(*source_pubkey, false)); accounts.push(AccountMeta::new_readonly(*delegate_pubkey, false)); accounts.push(AccountMeta::new_readonly( *owner_pubkey, signer_pubkeys.is_empty(), )); for signer_pubkey in signer_pubkeys.iter() { accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); } Ok(Instruction { program_id: *token_program_id, accounts, data, }) }
process_approve
function processes an Approve
instruction.Parameters
program_id
: The public key of the program invoked.
accounts
: A slice ofAccountInfo
objects representing the accounts involved in the approval process.
amount
: The amount of tokens to delegate.
expected_decimals
: An optional parameter to check if the mint's decimals match the expected decimals.
Steps:
- Account Iteration and Extraction:
Iterates over the provided accounts to extract relevant account information.
- Unpack Source Account:
Unpacks the data of the source account.
- Frozen Account Check:
Checks if the source account is frozen. If it is, the function returns an error.
- Expected Mint Info Check:
If
expected_decimals
is provided, the function unpacks the mint account data and checks if the mint's decimals match the expected decimals.- Owner Validation:
Validates the owner of the source account by calling
Self::validate_owner
.- Set Delegate and Amount:
Sets the delegate and the delegated amount in the source account.
- Pack Updated Source Account:
Packs the updated source account data back into the source account.
- Return Success.
/// ---token/program/src/processor.rs--- /// Processes an [Approve](enum.TokenInstruction.html) instruction. pub fn process_approve( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, expected_decimals: Option<u8>, ) -> ProgramResult { // Account Iteration and Extraction: let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let expected_mint_info = if let Some(expected_decimals) = expected_decimals { Some((next_account_info(account_info_iter)?, expected_decimals)) } else { None }; let delegate_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; // Unpack Source Account: let mut source_account = Account::unpack(&source_account_info.data.borrow())?; // Frozen Account Check: if source_account.is_frozen() { return Err(TokenError::AccountFrozen.into()); } // Expected Mint Info Check: if let Some((mint_info, expected_decimals)) = expected_mint_info { if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { return Err(TokenError::MintMismatch.into()); } let mint = Mint::unpack(&mint_info.data.borrow_mut())?; if expected_decimals != mint.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } } // Owner Validation: Self::validate_owner( program_id, &source_account.owner, owner_info, account_info_iter.as_slice(), )?; // Set Delegate and Amount: source_account.delegate = COption::Some(*delegate_info.key); source_account.delegated_amount = amount; Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; Ok(()) }
Burn Token
burn
operation is similar to ERC20’s. It burns certain amount of token. The owner or the delegate of the token account has authorit to perform
the burn operation.Different with
approve
instruction, burn
instruction needs to modify mint
account’s supply data, so the mint
account should be writable./// ---token/program/src/instruction.rs--- /// Creates a `Burn` instruction. pub fn burn( token_program_id: &Pubkey, account_pubkey: &Pubkey, mint_pubkey: &Pubkey, authority_pubkey: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, ) -> Result<Instruction, ProgramError> { check_program_account(token_program_id)?; let data = TokenInstruction::Burn { amount }.pack(); let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); accounts.push(AccountMeta::new(*account_pubkey, false)); accounts.push(AccountMeta::new(*mint_pubkey, false)); accounts.push(AccountMeta::new_readonly( *authority_pubkey, signer_pubkeys.is_empty(), )); for signer_pubkey in signer_pubkeys.iter() { accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); } Ok(Instruction { program_id: *token_program_id, accounts, data, }) }
process_burn
function processes a Burn
instruction, which reduces the number of tokens in an account and decreases the total supply of the tokens in the mint account
.Parameters
program_id
: The public key of the program invoking this function.
accounts
: A slice ofAccountInfo
objects representing the accounts involved in the burn process.
amount
: The amount of tokens to burn.
expected_decimals
: An optional parameter to check if the mint's decimals match the expected decimals.
Steps:
- Account Iteration and Extraction:
Iterates over the provided accounts to extract relevant account information.
- Unpack Source Account and Mint:
Unpacks the data of the source account and the mint account.
- Frozen Account Check:
Checks if the source account is frozen. If it is, the function returns an error.
- Native Token Check:
Checks if the source account is a native token account. If it is, the function returns an error.
- Sufficient Funds Check:
Ensures the source account has enough tokens to burn the specified amount. If not, the function returns an error.
- Mint Match Check:
Checks if the mint of the source account matches the mint provided. If not, the function returns an error.
- Decimals Check:
If expected_decimals is provided, checks if the mint's decimals match the expected decimals. If not, the function returns an error.
- Delegate and Owner Validation:
Validates the authority based on whether the source account has a delegate set. It also checks the delegated amount and updates it accordingly.
- Zero Amount Check:
If the amount is zero, it checks that the program owns both the source and mint accounts. Passing this check whether amount is 0 is to save computation unit, in that case, the runtime will throw error because the program has no authority to modify the account data.
- Update Balance And Supply:
Updates the source account balance and the mint's total supply by subtracting the burned amount.
- Pack Updated Accounts:
Packs the updated source account and mint data back into their respective accounts.
- Return Success.
Note that native token account is not supported to perform burn operation in the Token Program. Because native token account actually stores lamport, if it just deducts some amount of lamports without crediting the same amount to another account, the runtime will throw error. Solana has
incinerator
address which is used to handle burn of lamport./// ---token/program/src/processor.rs--- /// Processes a [Burn](enum.TokenInstruction.html) instruction. pub fn process_burn( program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, expected_decimals: Option<u8>, ) -> ProgramResult { // Account Iteration and Extraction: let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; // Unpack Source Account and Mint: let mut source_account = Account::unpack(&source_account_info.data.borrow())?; let mut mint = Mint::unpack(&mint_info.data.borrow())?; // Frozen Account Check: if source_account.is_frozen() { return Err(TokenError::AccountFrozen.into()); } // Native Token Check: if source_account.is_native() { return Err(TokenError::NativeNotSupported.into()); } // Sufficient Funds Check: if source_account.amount < amount { return Err(TokenError::InsufficientFunds.into()); } // Mint Match Check: if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) { return Err(TokenError::MintMismatch.into()); } // Decimals Check: if let Some(expected_decimals) = expected_decimals { if expected_decimals != mint.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } } // Delegate and Owner Validation: if !source_account.is_owned_by_system_program_or_incinerator() { match source_account.delegate { COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => { Self::validate_owner( program_id, delegate, authority_info, account_info_iter.as_slice(), )?; if source_account.delegated_amount < amount { return Err(TokenError::InsufficientFunds.into()); } source_account.delegated_amount = source_account .delegated_amount .checked_sub(amount) .ok_or(TokenError::Overflow)?; if source_account.delegated_amount == 0 { source_account.delegate = COption::None; } } _ => Self::validate_owner( program_id, &source_account.owner, authority_info, account_info_iter.as_slice(), )?, } } // Zero Amount Check: if amount == 0 { Self::check_account_owner(program_id, source_account_info)?; Self::check_account_owner(program_id, mint_info)?; } // Update Balance And Supply source_account.amount = source_account .amount .checked_sub(amount) .ok_or(TokenError::Overflow)?; mint.supply = mint .supply .checked_sub(amount) .ok_or(TokenError::Overflow)?; // Pack Updated Accounts: Account::pack(source_account, &mut source_account_info.data.borrow_mut())?; Mint::pack(mint, &mut mint_info.data.borrow_mut())?; Ok(()) }
Lamport Incinerator
/// ---token/program/src/state.rs--- /// Checks if a token Account's owner is the system_program or the /// incinerator pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { solana_program::system_program::check_id(&self.owner) || solana_program::incinerator::check_id(&self.owner) }
/// ---solana-program-1.18.11/src/incinerator.rs--- //! A designated address for burning lamports. //! //! Lamports credited to this address will be removed from the total supply //! (burned) at the end of the current block. crate::declare_id!("1nc1nerator11111111111111111111111111111111");
Conclusion
Solana Token Program is similar to Ethereum’s ERC20 implementation, but it has its own special design.
- Token’s metadata, like supply, decimal and mint authority are stored in a separate account. And wallets’ token ownership are stored in seprate token account respectively. This design helps improve token operation in parallel.
- In Solana, there isn't a concept of a "zero address" in the same way as Ethereum's
0x0
address. But there is a incinerator address which is used to burn lamports.
- Solana has deployed a native general purpose Token Program which can be used to create new token. In Ethereum, developers build and deploy their own ERC20. The general purpose Token Program ease the burnen of developer to build token program themselves, but it also introduce two problems. The first is the complicity of the Token Program, especially for Token Program 2022. Because it’s designed to be a general purpose Token Program, so there are many extensions implemented which increases computation unit. The second point is that it is not conducive to innovation. In Ethereum, ERC20 has a minimal functional implementation, such as the OpenZeppelin ERC20 implementation. Additional features are designed as modular components, allowing developers to inherit the desired contracts and implement different functionalities. This gives Ethereum's ERC20 a very high degree of flexibility. The simple account model in Ethereum also promotes design innovation.
- Due to VM differences, the authority verification is also quite different.
Generally, the Token Program implementation in Solana is innovative, it allows developers to deploy token quickly. The design of token model is based on the solana’s runtime design, enables token operations to be executed in parallel, which improves transactions throughput significantly.
Appendix
Calculate ATA account length
In the ATA creation operation, associated token program calculates the ATA’s data length based on which the rent is decided.
Inside the program, it invokes SPL Token Program 2022’s
GetAccountDataSize
function to get the data size. But it will call Original Token Program or Token Program 2022 based on the mint account’s owner. Because Token Program 2022’s GetAccountDataSize
instruction is compatible with Original Program’s, so it’s ok to use spl_token_2022::instruction::get_account_data_size
to construct the instruction. In the case its the Original Token Program to be called, the extension_types
won’t be used in the instruction process./// ---/associated-token-account/program/src/tools/account.rs--- /// Determines the required initial data length for a new token account based on /// the extensions initialized on the Mint pub fn get_account_len<'a>( mint: &AccountInfo<'a>, spl_token_program: &AccountInfo<'a>, extension_types: &[ExtensionType], ) -> Result<usize, ProgramError> { invoke( &spl_token_2022::instruction::get_account_data_size( spl_token_program.key, mint.key, extension_types, )?, &[mint.clone(), spl_token_program.clone()], )?; get_return_data() .ok_or(ProgramError::InvalidInstructionData) .and_then(|(key, data)| { if key != *spl_token_program.key { return Err(ProgramError::IncorrectProgramId); } data.try_into() .map(usize::from_le_bytes) .map_err(|_| ProgramError::InvalidInstructionData) }) }
Token Program 2022 constructs
GetAccountDataSize
instruction./// ---/token/program-2022/src/instruction.rs--- /// Creates a `GetAccountDataSize` instruction pub fn get_account_data_size( token_program_id: &Pubkey, mint_pubkey: &Pubkey, extension_types: &[ExtensionType], ) -> Result<Instruction, ProgramError> { check_spl_token_program_account(token_program_id)?; Ok(Instruction { program_id: *token_program_id, accounts: vec![AccountMeta::new_readonly(*mint_pubkey, false)], data: TokenInstruction::GetAccountDataSize { extension_types: extension_types.to_vec(), } .pack(), }) }
The instruction index(in the enum) is same in two Token Programs, and Original Token Program only uses the first byte of serialized instruction data to decide the instruction.
/// ---token/program/src/processor.rs--- /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenInstruction::unpack(input)?; match instruction { // ... TokenInstruction::GetAccountDataSize => { msg!("Instruction: GetAccountDataSize"); Self::process_get_account_data_size(program_id, accounts) } // ... } } /// ---token/program/src/instruction.rs--- pub fn unpack(input: &'a [u8]) -> Result<Self, ProgramError> { use TokenError::InvalidInstruction; let (&tag, rest) = input.split_first().ok_or(InvalidInstruction)?; Ok(match tag { // ... 21 => Self::GetAccountDataSize, // ... }) }
Process GetAccountDataSize
instruction In Token Program.
Step:
- iterate to get
mint
account.
- check the
mint
account’s owner if Token Program.
- unpack the
mint
account to check data consistency.
- call
set_return_data
to update the data length data to transaction return data buffer, so that the invoker(assosicated token program) can get the data.
/// ---token/program/src/processor.rs--- /// Processes a [GetAccountDataSize](enum.TokenInstruction.html) instruction pub fn process_get_account_data_size( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // make sure the mint is valid let mint_info = next_account_info(account_info_iter)?; Self::check_account_owner(program_id, mint_info)?; let _ = Mint::unpack(&mint_info.data.borrow()) .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?; set_return_data(&Account::LEN.to_le_bytes()); Ok(()) }
Process GetAccountDataSize
instruction In Token Program 2022.
Step:
- Verify all extensions to be implemented on
Account
indeed work onAccount
.
- Iterate
accounts
to get related account’s info.
- Get account extensions required by the mint account.
- Extend new extension types to get all extensions the token account needs implement.
- Calculate account length.
- Call
set_return_data
to update the data length data to transaction return data buffer, so that the invoker(assosicated token program) can get the data.
/// ---/token/program-2022/src/processor.rs--- /// Processes a [GetAccountDataSize](enum.TokenInstruction.html) instruction pub fn process_get_account_data_size( accounts: &[AccountInfo], new_extension_types: &[ExtensionType], ) -> ProgramResult { if new_extension_types .iter() .any(|&t| t.get_account_type() != AccountType::Account) { return Err(TokenError::ExtensionTypeMismatch.into()); } let account_info_iter = &mut accounts.iter(); let mint_account_info = next_account_info(account_info_iter)?; let mut account_extensions = Self::get_required_account_extensions(mint_account_info)?; // ExtensionType::try_calculate_account_len() dedupes types, so just a dumb // concatenation is fine here account_extensions.extend_from_slice(new_extension_types); let account_len = ExtensionType::try_calculate_account_len::<Account>(&account_extensions)?; set_return_data(&account_len.to_le_bytes()); Ok(()) }
get_required_account_extensions
decodes extensions required by the mint
account./// ---token/program-2022/src/processor.rs--- fn get_required_account_extensions( mint_account_info: &AccountInfo, ) -> Result<Vec<ExtensionType>, ProgramError> { let mint_data = mint_account_info.data.borrow(); // decode mint account data let state = PodStateWithExtensions::<PodMint>::unpack(&mint_data) .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?; // decode required extensions from state Self::get_required_account_extensions_from_unpacked_mint(mint_account_info.owner, &state) } fn get_required_account_extensions_from_unpacked_mint( token_program_id: &Pubkey, state: &PodStateWithExtensions<PodMint>, ) -> Result<Vec<ExtensionType>, ProgramError> { // check the owner of the mint account is SPL Token Program 2022 check_program_account(token_program_id)?; // get required extensions of mint account from mint account's state let mint_extensions = state.get_extension_types()?; // return required extensions Ok(ExtensionType::get_required_init_account_extensions( &mint_extensions, )) } /// ---token/program-2022/src/lib.rs--- /// Checks that the supplied program ID is correct for spl-token-2022 pub fn check_program_account(spl_token_program_id: &Pubkey) -> ProgramResult { if spl_token_program_id != &id() { return Err(ProgramError::IncorrectProgramId); } Ok(()) }
try_calculate_account_len
calcualtes the account’s data length considering all extensions./// ---token/program-2022/src/extension/mod.rs--- /// Get the required account data length for the given ExtensionTypes /// /// Fails if any of the extension types has a variable length pub fn try_calculate_account_len<S: BaseState>( extension_types: &[Self], ) -> Result<usize, ProgramError> { if extension_types.is_empty() { Ok(S::SIZE_OF) } else { let extension_size = Self::try_get_total_tlv_len(extension_types)?; let total_len = extension_size.saturating_add(BASE_ACCOUNT_AND_TYPE_LENGTH); Ok(adjust_len_for_multisig(total_len)) } } /// Get the TLV length for a set of ExtensionTypes /// /// Fails if any of the extension types has a variable length fn try_get_total_tlv_len(extension_types: &[Self]) -> Result<usize, ProgramError> { // dedupe extensions let mut extensions = vec![]; for extension_type in extension_types { if !extensions.contains(&extension_type) { extensions.push(extension_type); } } extensions.iter().map(|e| e.try_get_tlv_len()).sum() } /// Get the TLV length for an ExtensionType /// /// Fails if the extension type has a variable length fn try_get_tlv_len(&self) -> Result<usize, ProgramError> { Ok(add_type_and_length_to_len(self.try_get_type_len()?)) }
Associated Token Program gets return data
After the invocation succeeds, Token Program or Token Program 2022 has written the account’ data length to the transaction return data buffer. And
get_account_len
get the return data, verifies the data is indeed written by the target program, decodes it and return to caller./// ---associated-token-account/program/src/tools/account.rs--- /// Determines the required initial data length for a new token account based on /// the extensions initialized on the Mint pub fn get_account_len<'a>( mint: &AccountInfo<'a>, spl_token_program: &AccountInfo<'a>, extension_types: &[ExtensionType], ) -> Result<usize, ProgramError> { invoke( &spl_token_2022::instruction::get_account_data_size( spl_token_program.key, mint.key, extension_types, )?, &[mint.clone(), spl_token_program.clone()], )?; get_return_data() .ok_or(ProgramError::InvalidInstructionData) .and_then(|(key, data)| { if key != *spl_token_program.key { return Err(ProgramError::IncorrectProgramId); } data.try_into() .map(usize::from_le_bytes) .map_err(|_| ProgramError::InvalidInstructionData) }) }
Note the transaction return data buffer is shared by all CPI calls, so it needs to check the program which writes the data. Below is the doc of the
get_return_data
in solana source code./// ---/solana-program-1.18.11/src/program/--- /// Get the return data from an invoked program. /// /// For every transaction there is a single buffer with maximum length /// [`MAX_RETURN_DATA`], paired with a [`Pubkey`] representing the program ID of /// the program that most recently set the return data. Thus the return data is /// a global resource and care must be taken to ensure that it represents what /// is expected: called programs are free to set or not set the return data; and /// the return data may represent values set by programs multiple calls down the /// call stack, depending on the circumstances of transaction execution. /// /// Return data is set by the callee with [`set_return_data`]. /// /// Return data is cleared before every CPI invocation — a program that /// has invoked no other programs can expect the return data to be `None`; if no /// return data was set by the previous CPI invocation, then this function /// returns `None`. /// /// Return data is not cleared after returning from CPI invocations — a /// program that has called another program may retrieve return data that was /// not set by the called program, but instead set by a program further down the /// call stack; or, if a program calls itself recursively, it is possible that /// the return data was not set by the immediate call to that program, but by a /// subsequent recursive call to that program. Likewise, an external RPC caller /// may see return data that was not set by the program it is directly calling, /// but by a program that program called. /// /// For more about return data see the [documentation for the return data proposal][rdp]. /// /// [rdp]: https://docs.solanalabs.com/proposals/return-data pub fn get_return_data() -> Option<(Pubkey, Vec<u8>)> { // ... }
Token Program 2022 Immutable Owner
Token Program 2022 supports multiple extensions. One extensions is to make token account’s owner immutable.
process_set_authority
in Token Program 2022 handles Token Account authority change. When we the authority is the account owner, it validates the current owner has signed this transaction, and deserialize the Token Account’ data to check whether there is
immutableOwner
extension. If immutableOwner
extension exists, then returns error. Also it checks CpiGuard
extension.After all the extension checks pass, it update the account
owner
, delegate
, delegated_amount
and close_authority
./// Processes a [SetAuthority](enum.TokenInstruction.html) instruction. pub fn process_set_authority( program_id: &Pubkey, accounts: &[AccountInfo], authority_type: AuthorityType, new_authority: PodCOption<Pubkey>, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let account_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); let mut account_data = account_info.data.borrow_mut(); if let Ok(mut account) = PodStateWithExtensionsMut::<PodAccount>::unpack(&mut account_data) { if account.base.is_frozen() { return Err(TokenError::AccountFrozen.into()); } match authority_type { AuthorityType::AccountOwner => { Self::validate_owner( program_id, &account.base.owner, authority_info, authority_info_data_len, account_info_iter.as_slice(), )?; if account.get_extension_mut::<ImmutableOwner>().is_ok() { return Err(TokenError::ImmutableOwner.into()); } if let Ok(cpi_guard) = account.get_extension::<CpiGuard>() { if cpi_guard.lock_cpi.into() && in_cpi() { return Err(TokenError::CpiGuardSetAuthorityBlocked.into()); } else if cpi_guard.lock_cpi.into() { return Err(TokenError::CpiGuardOwnerChangeBlocked.into()); } } if let PodCOption { option: PodCOption::<Pubkey>::SOME, value: authority, } = new_authority { account.base.owner = authority; } else { return Err(TokenError::InvalidInstruction.into()); } account.base.delegate = PodCOption::none(); account.base.delegated_amount = 0.into(); if account.base.is_native() { account.base.close_authority = PodCOption::none(); } } // ... }
The extension info of Token Account are stored in the account’ data in TLV format (Tag, length, value).
get_tlv_data_mut()
returns extensions TLV data. get_extension_bytes_mut
loops extensions to check whether the target extension exists./// ---token/program-2022/src/extension/mod.rs--- /// Encapsulates mutable base state data (mint or account) with possible /// extensions #[derive(Debug, PartialEq)] pub struct StateWithExtensionsMut<'data, S: BaseState> { /// Unpacked base data pub base: S, /// Raw base data base_data: &'data mut [u8], /// Writable account type account_type: &'data mut [u8], /// Slice of data containing all TLV data, deserialized on demand tlv_data: &'data mut [u8], } impl<'a, S: BaseState> BaseStateWithExtensionsMut<S> for StateWithExtensionsMut<'a, S> { fn get_tlv_data_mut(&mut self) -> &mut [u8] { self.tlv_data } fn get_account_type_mut(&mut self) -> &mut [u8] { self.account_type } } /// Unpack a portion of the TLV data as the desired type that allows /// modifying the type fn get_extension_mut<V: Extension + Pod>(&mut self) -> Result<&mut V, ProgramError> { pod_from_bytes_mut::<V>(self.get_extension_bytes_mut::<V>()?) } /// Unpack a portion of the TLV data as the base mutable bytes fn get_extension_bytes_mut<V: Extension>(&mut self) -> Result<&mut [u8], ProgramError> { get_extension_bytes_mut::<S, V>(self.get_tlv_data_mut()) }
/// ---token/program-2022/src/extension/mod.rs------ fn get_extension_bytes_mut<S: BaseState, V: Extension>( tlv_data: &mut [u8], ) -> Result<&mut [u8], ProgramError> { if V::TYPE.get_account_type() != S::ACCOUNT_TYPE { return Err(ProgramError::InvalidAccountData); } let TlvIndices { type_start: _, length_start, value_start, } = get_extension_indices::<V>(tlv_data, false)?; // get_extension_indices has checked that tlv_data is long enough to include // these indices let length = pod_from_bytes::<Length>(&tlv_data[length_start..value_start])?; let value_end = value_start.saturating_add(usize::from(*length)); if tlv_data.len() < value_end { return Err(ProgramError::InvalidAccountData); } Ok(&mut tlv_data[value_start..value_end]) } fn get_extension_indices<V: Extension>( tlv_data: &[u8], init: bool, ) -> Result<TlvIndices, ProgramError> { let mut start_index = 0; let v_account_type = V::TYPE.get_account_type(); while start_index < tlv_data.len() { let tlv_indices = get_tlv_indices(start_index); if tlv_data.len() < tlv_indices.value_start { return Err(ProgramError::InvalidAccountData); } let extension_type = ExtensionType::try_from(&tlv_data[tlv_indices.type_start..tlv_indices.length_start])?; let account_type = extension_type.get_account_type(); if extension_type == V::TYPE { // found an instance of the extension that we're initializing, return! return Ok(tlv_indices); // got to an empty spot, init here, or error if we're searching, since // nothing is written after an Uninitialized spot } else if extension_type == ExtensionType::Uninitialized { if init { return Ok(tlv_indices); } else { return Err(TokenError::ExtensionNotFound.into()); } } else if v_account_type != account_type { return Err(TokenError::ExtensionTypeMismatch.into()); } else { let length = pod_from_bytes::<Length>( &tlv_data[tlv_indices.length_start..tlv_indices.value_start], )?; let value_end_index = tlv_indices.value_start.saturating_add(usize::from(*length)); start_index = value_end_index; } } Err(ProgramError::InvalidAccountData) }