22 KiB
Built-in Programs
This reference provides comprehensive coverage of Solana's built-in programs for native Rust development, focusing on the System Program and Compute Budget Program.
Table of Contents
- Overview of Built-in Programs
- System Program
- Compute Budget Program
- Other Built-in Programs
- CPI Patterns
- Best Practices
Overview of Built-in Programs
Built-in programs (also called native programs) are fundamental Solana programs that provide core blockchain functionality.
Key Built-in Programs
| Program | Program ID | Purpose |
|---|---|---|
| System Program | 11111111111111111111111111111111 |
Account creation, transfers, allocation |
| Compute Budget | ComputeBudget111111111111111111111111111111 |
CU limits, heap size, priority fees |
| BPF Loader | Various | Loading and executing programs |
| Config Program | Config1111111111111111111111111111111111111 |
Validator configuration |
| Stake Program | Stake11111111111111111111111111111111111111 |
Staking and delegation |
| Vote Program | Vote111111111111111111111111111111111111111 |
Validator voting |
This reference focuses on the two most commonly used in program development: System Program and Compute Budget Program.
System Program
Program ID: solana_program::system_program::ID (11111111111111111111111111111111)
The System Program is responsible for account creation, lamport transfers, and account management.
Core Functionality
- Create accounts (regular and PDAs)
- Transfer lamports between accounts
- Allocate space for account data
- Assign ownership to programs
- Create nonce accounts for durable transactions
System Program Instructions
use solana_program::system_instruction;
pub enum SystemInstruction {
CreateAccount, // Create new account
Assign, // Assign account to program
Transfer, // Transfer lamports
CreateAccountWithSeed,// Create account with seed
AdvanceNonceAccount, // Advance nonce
WithdrawNonceAccount, // Withdraw from nonce
InitializeNonceAccount, // Initialize nonce
Allocate, // Allocate account space
AllocateWithSeed, // Allocate with seed
AssignWithSeed, // Assign with seed
TransferWithSeed, // Transfer with seed
UpgradeNonceAccount, // Upgrade nonce (v4)
}
CreateAccount
Creates a new account with lamports and data space.
Function Signature
pub fn create_account(
from_pubkey: &Pubkey, // Funding account (must be signer)
to_pubkey: &Pubkey, // New account address
lamports: u64, // Lamports to fund account
space: u64, // Bytes of data space
owner: &Pubkey, // Program that will own the account
) -> Instruction
Usage in Native Rust
use solana_program::{
system_instruction,
program::invoke,
};
pub fn create_new_account(
payer: &AccountInfo,
new_account: &AccountInfo,
system_program: &AccountInfo,
program_id: &Pubkey,
) -> ProgramResult {
let space = 100; // Account data size
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
let create_account_ix = system_instruction::create_account(
payer.key,
new_account.key,
lamports,
space as u64,
program_id,
);
invoke(
&create_account_ix,
&[
payer.clone(),
new_account.clone(),
system_program.clone(),
],
)?;
msg!("Created account with {} bytes", space);
Ok(())
}
Creating PDA Accounts
use solana_program::program::invoke_signed;
pub fn create_pda_account(
payer: &AccountInfo,
pda_account: &AccountInfo,
system_program: &AccountInfo,
program_id: &Pubkey,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Verify PDA
let (expected_pda, _bump) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
let space = 200;
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
let create_account_ix = system_instruction::create_account(
payer.key,
pda_account.key,
lamports,
space as u64,
program_id,
);
// Create full seeds with bump
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&create_account_ix,
&[payer.clone(), pda_account.clone(), system_program.clone()],
signer_seeds,
)?;
msg!("Created PDA account at {}", pda_account.key);
Ok(())
}
Transfer
Transfers lamports from one account to another.
Function Signature
pub fn transfer(
from_pubkey: &Pubkey, // Source account (must be signer)
to_pubkey: &Pubkey, // Destination account
lamports: u64, // Amount to transfer
) -> Instruction
Usage in Native Rust
pub fn transfer_lamports(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
let transfer_ix = system_instruction::transfer(
from.key,
to.key,
amount,
);
invoke(
&transfer_ix,
&[from.clone(), to.clone(), system_program.clone()],
)?;
msg!("Transferred {} lamports from {} to {}",
amount, from.key, to.key);
Ok(())
}
Transfer from PDA
pub fn transfer_from_pda(
pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
let transfer_ix = system_instruction::transfer(
pda.key,
to.key,
amount,
);
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&transfer_ix,
&[pda.clone(), to.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
Allocate
Allocates space for an account's data.
Function Signature
pub fn allocate(
pubkey: &Pubkey, // Account to allocate (must be signer)
space: u64, // Bytes to allocate
) -> Instruction
Usage in Native Rust
pub fn allocate_account_space(
account: &AccountInfo,
system_program: &AccountInfo,
space: u64,
) -> ProgramResult {
let allocate_ix = system_instruction::allocate(
account.key,
space,
);
invoke(
&allocate_ix,
&[account.clone(), system_program.clone()],
)?;
msg!("Allocated {} bytes for account", space);
Ok(())
}
⚠️ Note: The account must be owned by the System Program before allocating. Most programs use create_account instead, which combines allocation with ownership assignment.
Assign
Assigns an account to a program (changes owner).
Function Signature
pub fn assign(
pubkey: &Pubkey, // Account to assign (must be signer)
owner: &Pubkey, // New owner program
) -> Instruction
Usage in Native Rust
pub fn assign_to_program(
account: &AccountInfo,
system_program: &AccountInfo,
new_owner: &Pubkey,
) -> ProgramResult {
let assign_ix = system_instruction::assign(
account.key,
new_owner,
);
invoke(
&assign_ix,
&[account.clone(), system_program.clone()],
)?;
msg!("Assigned account to program {}", new_owner);
Ok(())
}
⚠️ Note: Most programs use create_account which handles assignment during creation.
Complete Example: Account Lifecycle
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserData {
pub user: Pubkey,
pub balance: u64,
pub created_at: i64,
}
pub fn create_user_account(
program_id: &Pubkey,
accounts: &[AccountInfo],
user_pubkey: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let user_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// 1. Derive PDA
let seeds = &[b"user", user_pubkey.as_ref()];
let (pda, bump) = Pubkey::find_program_address(seeds, program_id);
if pda != *user_account.key {
return Err(ProgramError::InvalidSeeds);
}
// 2. Calculate space and rent
let space = std::mem::size_of::<UserData>();
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
// 3. Create account via System Program CPI
let create_ix = system_instruction::create_account(
payer.key,
user_account.key,
lamports,
space as u64,
program_id,
);
let signer_seeds: &[&[&[u8]]] = &[&[b"user", user_pubkey.as_ref(), &[bump]]];
invoke_signed(
&create_ix,
&[payer.clone(), user_account.clone(), system_program.clone()],
signer_seeds,
)?;
// 4. Initialize account data
let clock = Clock::get()?;
let user_data = UserData {
user: user_pubkey,
balance: 0,
created_at: clock.unix_timestamp,
};
user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
msg!("Created user account for {}", user_pubkey);
Ok(())
}
Compute Budget Program
Program ID: solana_program::compute_budget::ID (ComputeBudget111111111111111111111111111111)
The Compute Budget Program allows transactions to request specific compute unit limits, heap sizes, and priority fees.
Core Functionality
- Set compute unit limit - Maximum CUs for transaction
- Set compute unit price - Priority fee per CU
- Request heap size - Heap memory allocation
Compute Budget Instructions
use solana_program::compute_budget::ComputeBudgetInstruction;
pub enum ComputeBudgetInstruction {
RequestUnitsDeprecated, // Deprecated
RequestHeapFrame(u32), // Request heap frame (bytes)
SetComputeUnitLimit(u32), // Set max CUs
SetComputeUnitPrice(u64), // Set priority fee (microlamports per CU)
SetLoadedAccountsDataSizeLimit(u32), // Set loaded accounts data limit
}
SetComputeUnitLimit
Sets the maximum compute units available to the transaction.
Function Signature
pub fn set_compute_unit_limit(units: u32) -> Instruction
Default Limits
- Default per instruction: 200,000 CUs
- Default per transaction: 1,400,000 CUs (with requested CU limit)
- Maximum: 1,400,000 CUs
Usage in Native Rust
Important: Compute Budget instructions are added to the transaction by the client, not inside the program.
Client-side example (for reference):
// This code runs CLIENT-SIDE, not in the program
use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
transaction::Transaction,
};
let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000);
let transaction = Transaction::new_signed_with_payer(
&[
compute_budget_ix, // Must be first
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
⚠️ Note: Programs cannot modify their own compute budget. These instructions must be added client-side before sending the transaction.
SetComputeUnitPrice
Sets the priority fee per compute unit (for transaction prioritization).
Function Signature
pub fn set_compute_unit_price(microlamports: u64) -> Instruction
Priority Fee Calculation
Total Priority Fee = (CUs Used × microlamports) / 1,000,000
Example:
- CUs used: 50,000
- Price: 10,000 microlamports per CU
- Fee: (50,000 × 10,000) / 1,000,000 = 500 lamports
Usage (Client-side)
// Client-side code
let compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(20_000);
let transaction = Transaction::new_signed_with_payer(
&[
compute_unit_price_ix, // Set priority fee
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
Use cases:
- High-priority transactions (arbitrage, liquidations)
- Congested network periods
- Time-sensitive operations
RequestHeapFrame
Requests additional heap memory for the transaction.
Function Signature
pub fn request_heap_frame(bytes: u32) -> Instruction
Default Heap
- Default: 32 KB
- Maximum: 256 KB
Usage (Client-side)
// Client-side code
let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(256 * 1024); // 256 KB
let transaction = Transaction::new_signed_with_payer(
&[
heap_size_ix, // Request more heap
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
When to use:
- Large data structures
- Complex deserialization
- Temporary buffers
⚠️ Cost: Requesting heap increases CU consumption.
SetLoadedAccountsDataSizeLimit
Sets the maximum total size of loaded account data.
Function Signature
pub fn set_loaded_accounts_data_size_limit(bytes: u32) -> Instruction
Default Limit
- Default: 64 MB per transaction
Usage (Client-side)
// Client-side code
let accounts_data_limit_ix =
ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(128 * 1024 * 1024);
let transaction = Transaction::new_signed_with_payer(
&[
accounts_data_limit_ix,
your_program_ix,
],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
Use cases:
- Transactions with many large accounts
- Bulk processing operations
Complete Client-side Example
use solana_sdk::{
compute_budget::ComputeBudgetInstruction,
transaction::Transaction,
signature::{Keypair, Signer},
pubkey::Pubkey,
};
pub fn build_optimized_transaction(
payer: &Keypair,
program_id: &Pubkey,
program_ix_data: &[u8],
accounts: Vec<AccountMeta>,
recent_blockhash: Hash,
) -> Transaction {
// 1. Set compute unit limit (if default 200k is insufficient)
let compute_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000);
// 2. Set priority fee (for faster processing)
let compute_price_ix = ComputeBudgetInstruction::set_compute_unit_price(10_000);
// 3. Request additional heap if needed
let heap_size_ix = ComputeBudgetInstruction::request_heap_frame(128 * 1024); // 128 KB
// 4. Your program instruction
let program_ix = Instruction {
program_id: *program_id,
accounts,
data: program_ix_data.to_vec(),
};
// 5. Build transaction (compute budget instructions FIRST)
Transaction::new_signed_with_payer(
&[
compute_limit_ix,
compute_price_ix,
heap_size_ix,
program_ix,
],
Some(&payer.pubkey()),
&[payer],
recent_blockhash,
)
}
Other Built-in Programs
BPF Loader
Purpose: Loads and executes Solana programs.
Program IDs:
BPFLoader1111111111111111111111111111111111(deprecated)BPFLoader2111111111111111111111111111111111(upgradeable)BPFLoaderUpgradeab1e11111111111111111111111(current)
Usage: Primarily used by the runtime. Programs rarely interact with BPF Loader directly.
Stake Program
Program ID: Stake11111111111111111111111111111111111111
Purpose: Staking SOL to validators.
Common operations:
- Create stake accounts
- Delegate stake
- Deactivate stake
- Withdraw stake
Use case: Staking pools, liquid staking protocols.
Vote Program
Program ID: Vote111111111111111111111111111111111111111
Purpose: Validator voting and consensus.
Use case: Validator operations, rarely used by general programs.
CPI Patterns
System Program CPI Pattern
Standard pattern for calling System Program:
use solana_program::{
program::invoke,
system_instruction,
};
pub fn system_program_cpi(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
) -> ProgramResult {
// 1. Verify System Program
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
// 2. Create instruction
let ix = system_instruction::transfer(from.key, to.key, 1_000_000);
// 3. Invoke
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
Ok(())
}
PDA Signing Pattern
When PDAs need to sign:
pub fn pda_system_cpi(
pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
program_id: &Pubkey,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// 1. Verify PDA
let (expected_pda, _) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda.key {
return Err(ProgramError::InvalidSeeds);
}
// 2. Create instruction
let ix = system_instruction::transfer(pda.key, to.key, 500_000);
// 3. Prepare signer seeds
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
// 4. Invoke with PDA signature
invoke_signed(
&ix,
&[pda.clone(), to.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
Validation Pattern
Always validate accounts before CPI:
pub fn safe_system_cpi(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
// ✅ Validate System Program
if system_program.key != &solana_program::system_program::ID {
msg!("Invalid System Program");
return Err(ProgramError::IncorrectProgramId);
}
// ✅ Validate signer
if !from.is_signer {
msg!("From account must be signer");
return Err(ProgramError::MissingRequiredSignature);
}
// ✅ Validate sufficient balance
if from.lamports() < amount {
msg!("Insufficient balance");
return Err(ProgramError::InsufficientFunds);
}
// Execute CPI
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
Ok(())
}
Best Practices
1. Always Validate Program IDs
// ✅ Validate before CPI
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
2. Use Rent Exemption
// ✅ Always create accounts with rent exemption
let rent = Rent::get()?;
let lamports = rent.minimum_balance(space);
// ❌ Don't use arbitrary amounts
let lamports = 1_000_000; // May not be rent-exempt!
3. Verify PDA Before Creation
// ✅ Verify PDA derivation
let (expected_pda, bump) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
4. Use invoke_signed for PDAs
// ✅ PDAs sign with invoke_signed
invoke_signed(&ix, accounts, signer_seeds)?;
// ❌ Regular invoke won't work for PDA signers
invoke(&ix, accounts)?; // Fails if PDA needs to sign
5. Set Compute Budget Client-side
// ✅ Add compute budget instructions in client
let ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(400_000),
your_program_ix,
];
// ❌ Cannot set from within program
// Programs cannot modify their own compute budget
6. Order Compute Budget Instructions First
// ✅ Compute budget instructions FIRST
let ixs = vec![
compute_limit_ix,
compute_price_ix,
heap_size_ix,
program_ix,
];
// ❌ Wrong order - may not apply
let ixs = vec![
program_ix,
compute_limit_ix, // Too late!
];
7. Check Account Ownership Before Transfer
// ✅ Validate ownership for security
if from_account.owner != &solana_program::system_program::ID {
msg!("Can only transfer from System-owned accounts");
return Err(ProgramError::IllegalOwner);
}
Summary
Key Takeaways:
- System Program handles account creation, transfers, and allocation
- Compute Budget Program instructions are added client-side, not in programs
- Always validate program IDs before CPI
- Use rent exemption when creating accounts
- PDAs require invoke_signed for signing operations
Most Common Operations:
| Operation | Instruction | Use Case |
|---|---|---|
| Create account | create_account |
New program accounts |
| Transfer lamports | transfer |
SOL transfers |
| Set CU limit | set_compute_unit_limit |
High-CU transactions |
| Set priority fee | set_compute_unit_price |
Fast transaction processing |
| Request heap | request_heap_frame |
Large data operations |
System Program CPI Template:
// Validate
if system_program.key != &solana_program::system_program::ID {
return Err(ProgramError::IncorrectProgramId);
}
// Create instruction
let ix = system_instruction::transfer(from.key, to.key, amount);
// Invoke (or invoke_signed for PDAs)
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])?;
Compute Budget Client Template:
// Client-side
let ixs = vec![
ComputeBudgetInstruction::set_compute_unit_limit(300_000),
ComputeBudgetInstruction::set_compute_unit_price(10_000),
your_program_ix,
];
Master these built-in programs for efficient account management and transaction optimization in production Solana programs.