Files
2025-11-30 09:01:25 +08:00

28 KiB

Native Rust Security Patterns for Solana Programs

This reference covers security vulnerabilities and best practices specific to Solana programs built with native Rust (without Anchor framework).

Table of Contents

  1. Manual Account Validation
  2. Account Discriminator Patterns
  3. PDA Security in Native Rust
  4. Manual CPI Security
  5. Manual Serialization Security
  6. Rent and Space Management
  7. Error Handling in Native Rust
  8. Token Program Integration
  9. Low-Level Security Patterns
  10. Native Rust Best Practices

Manual Account Validation

In native Rust programs, ALL account validation must be performed manually. Missing any check can lead to critical vulnerabilities.

Signer Checks

Vulnerable:

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let authority = next_account_info(account_info_iter)?;

    // Missing signer check - anyone can call this!
    // Perform privileged operation
    Ok(())
}

Secure:

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let authority = next_account_info(account_info_iter)?;

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

    // Now safe to perform privileged operation
    Ok(())
}

Owner Validation

Vulnerable:

pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let config_account = next_account_info(account_info_iter)?;

    // Missing owner check - could be any account!
    let mut config_data = Config::try_from_slice(&config_account.data.borrow())?;
    config_data.value = 42;
    config_data.serialize(&mut *config_account.data.borrow_mut())?;

    Ok(())
}

Secure:

pub fn update_config(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let config_account = next_account_info(account_info_iter)?;

    // Verify this account is owned by our program
    if config_account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }

    let mut config_data = Config::try_from_slice(&config_account.data.borrow())?;
    config_data.value = 42;
    config_data.serialize(&mut *config_account.data.borrow_mut())?;

    Ok(())
}

Writable Checks

Vulnerable:

pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let source = next_account_info(account_info_iter)?;

    // Missing writable check - runtime will panic!
    let mut data = source.try_borrow_mut_data()?;
    // Modify data...
    Ok(())
}

Secure:

pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let source = next_account_info(account_info_iter)?;

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

    let mut data = source.try_borrow_mut_data()?;
    // Safe to modify data
    Ok(())
}

Comprehensive Validation Function

Best Practice:

pub struct AccountValidation<'a, 'info> {
    account: &'a AccountInfo<'info>,
}

impl<'a, 'info> AccountValidation<'a, 'info> {
    pub fn new(account: &'a AccountInfo<'info>) -> Self {
        Self { account }
    }

    pub fn owner(self, expected_owner: &Pubkey) -> Result<Self, ProgramError> {
        if self.account.owner != expected_owner {
            return Err(ProgramError::IncorrectProgramId);
        }
        Ok(self)
    }

    pub fn signer(self) -> Result<Self, ProgramError> {
        if !self.account.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }
        Ok(self)
    }

    pub fn writable(self) -> Result<Self, ProgramError> {
        if !self.account.is_writable {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(self)
    }

    pub fn key(self, expected_key: &Pubkey) -> Result<Self, ProgramError> {
        if self.account.key != expected_key {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(self)
    }

    pub fn initialized(self) -> Result<Self, ProgramError> {
        if self.account.data_is_empty() {
            return Err(ProgramError::UninitializedAccount);
        }
        Ok(self)
    }
}

// Usage:
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let authority = next_account_info(account_info_iter)?;
    let config = next_account_info(account_info_iter)?;

    AccountValidation::new(authority)
        .signer()?;

    AccountValidation::new(config)
        .owner(program_id)?
        .writable()?
        .initialized()?;

    // All validations passed
    Ok(())
}

Account Discriminator Patterns

Without Anchor's automatic discriminators, you must manually implement account type safety.

Why Discriminators Matter

Vulnerable:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct ConfigAccount {
    pub admin: Pubkey,
    pub value: u64,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
    pub owner: Pubkey,
    pub balance: u64,
}

pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult {
    let config = next_account_info(&mut accounts.iter())?;

    // No discriminator check - UserAccount has same layout!
    let mut data = ConfigAccount::try_from_slice(&config.data.borrow())?;
    data.value = 999;
    // Could be writing to a UserAccount!

    Ok(())
}

Implementing Discriminators

Secure:

use borsh::{BorshDeserialize, BorshSerialize};

pub const CONFIG_DISCRIMINATOR: u64 = 0x1234567890ABCDEF;
pub const USER_DISCRIMINATOR: u64 = 0xFEDCBA0987654321;

#[derive(BorshSerialize, BorshDeserialize)]
pub struct ConfigAccount {
    pub discriminator: u64,
    pub admin: Pubkey,
    pub value: u64,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
    pub discriminator: u64,
    pub owner: Pubkey,
    pub balance: u64,
}

impl ConfigAccount {
    pub const LEN: usize = 8 + 32 + 8;

    pub fn new(admin: Pubkey, value: u64) -> Self {
        Self {
            discriminator: CONFIG_DISCRIMINATOR,
            admin,
            value,
        }
    }

    pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
        let data = account.data.borrow();
        if data.len() < 8 {
            return Err(ProgramError::InvalidAccountData);
        }

        let discriminator = u64::from_le_bytes(data[0..8].try_into().unwrap());
        if discriminator != CONFIG_DISCRIMINATOR {
            return Err(ProgramError::InvalidAccountData);
        }

        Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData)
    }
}

pub fn update_config(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let config_account = next_account_info(&mut accounts.iter())?;

    // Discriminator validated during deserialization
    let mut config = ConfigAccount::from_account_info(config_account)?;
    config.value = 999;
    config.serialize(&mut *config_account.data.borrow_mut())?;

    Ok(())
}

Alternative: String-Based Discriminators

pub const ACCOUNT_TYPE_LEN: usize = 8;

#[derive(BorshSerialize, BorshDeserialize)]
pub struct TaggedAccount {
    pub account_type: [u8; ACCOUNT_TYPE_LEN], // "CONFIG\0\0"
    pub data: AccountData,
}

impl TaggedAccount {
    pub fn new_config(data: AccountData) -> Self {
        let mut account_type = [0u8; ACCOUNT_TYPE_LEN];
        account_type[..6].copy_from_slice(b"CONFIG");
        Self { account_type, data }
    }

    pub fn assert_config(&self) -> ProgramResult {
        let mut expected = [0u8; ACCOUNT_TYPE_LEN];
        expected[..6].copy_from_slice(b"CONFIG");

        if self.account_type != expected {
            return Err(ProgramError::InvalidAccountData);
        }
        Ok(())
    }
}

PDA Security in Native Rust

find_program_address vs create_program_address

Vulnerable:

pub fn init_pda(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    bump: u8,
) -> ProgramResult {
    let pda_account = next_account_info(&mut accounts.iter())?;

    // Using user-provided bump without validation!
    let pda = Pubkey::create_program_address(
        &[b"config", &[bump]],
        program_id,
    )?;

    if pda_account.key != &pda {
        return Err(ProgramError::InvalidAccountData);
    }

    // Attacker could find non-canonical bump
    Ok(())
}

Secure:

pub fn init_pda(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let pda_account = next_account_info(&mut accounts.iter())?;

    // Always use find_program_address to get canonical bump
    let (pda, bump) = Pubkey::find_program_address(
        &[b"config"],
        program_id,
    );

    if pda_account.key != &pda {
        return Err(ProgramError::InvalidAccountData);
    }

    // Store the canonical bump for later use
    let mut data = ConfigPda::new(bump);
    data.serialize(&mut *pda_account.data.borrow_mut())?;

    Ok(())
}

Storing and Using Canonical Bumps

Best Practice:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultPda {
    pub discriminator: u64,
    pub bump: u8,
    pub authority: Pubkey,
    pub balance: u64,
}

impl VaultPda {
    pub fn seeds<'a>(&'a self, authority: &'a Pubkey) -> [&'a [u8]; 3] {
        [b"vault", authority.as_ref(), &[self.bump]]
    }

    pub fn verify_pda(
        &self,
        pda_account: &AccountInfo,
        authority: &Pubkey,
        program_id: &Pubkey,
    ) -> ProgramResult {
        let expected_pda = Pubkey::create_program_address(
            &self.seeds(authority),
            program_id,
        )?;

        if pda_account.key != &expected_pda {
            return Err(ProgramError::InvalidSeeds);
        }

        Ok(())
    }
}

PDA Signing with invoke_signed

Secure Pattern:

use solana_program::program::invoke_signed;

pub fn transfer_from_pda(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let vault_pda = next_account_info(account_info_iter)?;
    let destination = next_account_info(account_info_iter)?;
    let authority = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // Load and validate PDA data
    let vault = VaultPda::from_account_info(vault_pda)?;
    vault.verify_pda(vault_pda, authority.key, program_id)?;

    // Sign with PDA's seeds
    let seeds = vault.seeds(authority.key);
    let signer_seeds = &[&seeds[..]];

    let ix = solana_program::system_instruction::transfer(
        vault_pda.key,
        destination.key,
        amount,
    );

    invoke_signed(
        &ix,
        &[vault_pda.clone(), destination.clone(), system_program.clone()],
        signer_seeds,
    )?;

    Ok(())
}

Preventing PDA Substitution

Vulnerable:

pub fn withdraw(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = next_account_info(&mut accounts.iter())?;

    // No validation that this is the CORRECT vault PDA
    let vault_data = VaultPda::from_account_info(vault)?;

    // Attacker could substitute a different vault!
    Ok(())
}

Secure:

pub fn withdraw(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let vault = next_account_info(account_info_iter)?;
    let authority = next_account_info(account_info_iter)?;

    // Derive expected PDA
    let (expected_vault, _bump) = Pubkey::find_program_address(
        &[b"vault", authority.key.as_ref()],
        program_id,
    );

    // Validate this is the correct PDA
    if vault.key != &expected_vault {
        return Err(ProgramError::InvalidAccountData);
    }

    let vault_data = VaultPda::from_account_info(vault)?;
    // Safe to proceed

    Ok(())
}

Manual CPI Security

Building AccountMeta Arrays Securely

Vulnerable:

pub fn dangerous_cpi(accounts: &[AccountInfo]) -> ProgramResult {
    let target_program = next_account_info(&mut accounts.iter())?;
    let account1 = next_account_info(&mut accounts.iter())?;

    // Missing validation - could be any program!
    let ix = Instruction {
        program_id: *target_program.key,
        accounts: vec![
            AccountMeta::new(*account1.key, false), // Wrong signer flag!
        ],
        data: vec![],
    };

    invoke(&ix, &[target_program.clone(), account1.clone()])?;
    Ok(())
}

Secure:

use solana_program::program::invoke;

pub const EXPECTED_PROGRAM_ID: Pubkey = solana_program::pubkey!("YourProgramID111111111111111111111111111111");

pub fn secure_cpi(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let target_program = next_account_info(account_info_iter)?;
    let account1 = next_account_info(account_info_iter)?;

    // Validate target program ID
    if target_program.key != &EXPECTED_PROGRAM_ID {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Correctly propagate signer/writable flags
    let account_metas = vec![
        AccountMeta {
            pubkey: *account1.key,
            is_signer: account1.is_signer,
            is_writable: account1.is_writable,
        },
    ];

    let ix = Instruction {
        program_id: *target_program.key,
        accounts: account_metas,
        data: vec![],
    };

    invoke(&ix, &[target_program.clone(), account1.clone()])?;
    Ok(())
}

Checking CPI Success

Best Practice:

pub fn cpi_with_validation(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let token_program = next_account_info(account_info_iter)?;
    let source = next_account_info(account_info_iter)?;
    let destination = next_account_info(account_info_iter)?;
    let authority = next_account_info(account_info_iter)?;

    // Get balances before CPI
    let source_before = source.lamports();
    let dest_before = destination.lamports();

    let ix = spl_token::instruction::transfer(
        token_program.key,
        source.key,
        destination.key,
        authority.key,
        &[],
        1000,
    )?;

    invoke(&ix, &[source.clone(), destination.clone(), authority.clone()])?;

    // Verify state changed as expected (for native SOL transfers)
    // Note: For SPL tokens, you'd need to deserialize token accounts

    Ok(())
}

Manual Serialization Security

Borsh Serialization Pitfalls

Vulnerable:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Config {
    pub value: u64,
    pub items: Vec<Item>, // Variable length!
}

pub fn deserialize_config(account: &AccountInfo) -> ProgramResult {
    // No size validation - could run out of compute!
    let config = Config::try_from_slice(&account.data.borrow())?;

    // Attacker could create huge Vec causing OOM
    for item in &config.items {
        // Process item
    }

    Ok(())
}

Secure:

pub const MAX_ITEMS: usize = 100;

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Config {
    pub value: u64,
    pub item_count: u32,
    pub items: Vec<Item>,
}

impl Config {
    pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
        let data = account.data.borrow();

        // Validate minimum size
        if data.len() < 8 + 4 {
            return Err(ProgramError::InvalidAccountData);
        }

        let config = Self::try_from_slice(&data)
            .map_err(|_| ProgramError::InvalidAccountData)?;

        // Validate item count matches actual length
        if config.item_count as usize != config.items.len() {
            return Err(ProgramError::InvalidAccountData);
        }

        // Enforce maximum items
        if config.items.len() > MAX_ITEMS {
            return Err(ProgramError::InvalidAccountData);
        }

        Ok(config)
    }
}

Account Data Layout Validation

Best Practice:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
    pub discriminator: u64,
    pub owner: Pubkey,
    pub balance: u64,
    pub created_at: i64,
}

impl UserAccount {
    pub const LEN: usize = 8 + 32 + 8 + 8;

    pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
        let data = account.data.borrow();

        // Exact size check prevents truncation attacks
        if data.len() != Self::LEN {
            return Err(ProgramError::InvalidAccountData);
        }

        Self::try_from_slice(&data)
            .map_err(|_| ProgramError::InvalidAccountData)
    }

    pub fn to_account_info(&self, account: &AccountInfo) -> ProgramResult {
        let mut data = account.data.borrow_mut();

        if data.len() != Self::LEN {
            return Err(ProgramError::InvalidAccountData);
        }

        self.serialize(&mut *data)
            .map_err(|_| ProgramError::InvalidAccountData)
    }
}

Rent and Space Management

Rent Exemption Validation

Secure Pattern:

use solana_program::rent::Rent;
use solana_program::sysvar::Sysvar;

pub fn create_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    space: usize,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let new_account = next_account_info(account_info_iter)?;
    let payer = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // Get rent sysvar
    let rent = Rent::get()?;

    // Calculate required lamports for rent exemption
    let required_lamports = rent.minimum_balance(space);

    // Validate account has enough lamports
    if new_account.lamports() < required_lamports {
        return Err(ProgramError::AccountNotRentExempt);
    }

    // Additional validation: account is rent exempt
    if !rent.is_exempt(new_account.lamports(), new_account.data_len()) {
        return Err(ProgramError::AccountNotRentExempt);
    }

    Ok(())
}

Account Size Calculation

Vulnerable:

pub fn init_account(space: usize) -> ProgramResult {
    // No validation - attacker could request huge space
    let ix = solana_program::system_instruction::create_account(
        &payer.key,
        &new_account.key,
        lamports,
        space as u64, // Could overflow!
        program_id,
    );

    Ok(())
}

Secure:

pub const MIN_ACCOUNT_SIZE: usize = 128;
pub const MAX_ACCOUNT_SIZE: usize = 10_240; // 10KB

pub fn init_account(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    requested_space: usize,
) -> ProgramResult {
    // Validate space within reasonable bounds
    if requested_space < MIN_ACCOUNT_SIZE || requested_space > MAX_ACCOUNT_SIZE {
        return Err(ProgramError::InvalidAccountData);
    }

    // Ensure space alignment
    let space = requested_space
        .checked_next_multiple_of(8)
        .ok_or(ProgramError::InvalidAccountData)?;

    let rent = Rent::get()?;
    let lamports = rent.minimum_balance(space);

    // Safe to create account
    Ok(())
}

Error Handling in Native Rust

Custom Error Types

Best Practice:

use thiserror::Error;
use solana_program::program_error::ProgramError;

#[derive(Error, Debug, Copy, Clone)]
pub enum MyProgramError {
    #[error("Invalid authority")]
    InvalidAuthority,

    #[error("Insufficient balance")]
    InsufficientBalance,

    #[error("Account already initialized")]
    AlreadyInitialized,

    #[error("Arithmetic overflow")]
    ArithmeticOverflow,
}

impl From<MyProgramError> for ProgramError {
    fn from(e: MyProgramError) -> Self {
        ProgramError::Custom(e as u32)
    }
}

pub fn process(accounts: &[AccountInfo]) -> Result<(), MyProgramError> {
    let authority = next_account_info(&mut accounts.iter())
        .map_err(|_| MyProgramError::InvalidAuthority)?;

    if !authority.is_signer {
        return Err(MyProgramError::InvalidAuthority);
    }

    Ok(())
}

Avoiding unwrap() and expect()

Vulnerable:

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let account = accounts.get(0).unwrap(); // Panics if no accounts!
    let data = account.data.borrow();
    let value = u64::from_le_bytes(data[0..8].try_into().unwrap()); // Panics if not 8 bytes!

    Ok(())
}

Secure:

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let account = accounts
        .get(0)
        .ok_or(ProgramError::NotEnoughAccountKeys)?;

    let data = account.data.borrow();

    if data.len() < 8 {
        return Err(ProgramError::InvalidAccountData);
    }

    let value = u64::from_le_bytes(
        data[0..8]
            .try_into()
            .map_err(|_| ProgramError::InvalidAccountData)?
    );

    Ok(())
}

Token Program Integration

Manual Token CPI Construction

Secure Pattern:

use spl_token::instruction as token_instruction;

pub fn transfer_tokens(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    amount: u64,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let source_token = next_account_info(account_info_iter)?;
    let dest_token = next_account_info(account_info_iter)?;
    let authority = next_account_info(account_info_iter)?;
    let token_program = next_account_info(account_info_iter)?;

    // Validate token program
    if token_program.key != &spl_token::id() {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Validate authority is signer
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Build transfer instruction
    let transfer_ix = token_instruction::transfer(
        token_program.key,
        source_token.key,
        dest_token.key,
        authority.key,
        &[],
        amount,
    )?;

    invoke(
        &transfer_ix,
        &[
            source_token.clone(),
            dest_token.clone(),
            authority.clone(),
            token_program.clone(),
        ],
    )?;

    Ok(())
}

Token Account Validation

Secure Pattern:

use spl_token::state::Account as TokenAccount;

pub fn validate_token_account(
    token_account_info: &AccountInfo,
    expected_owner: &Pubkey,
    expected_mint: &Pubkey,
) -> Result<TokenAccount, ProgramError> {
    // Verify owned by token program
    if token_account_info.owner != &spl_token::id() {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Deserialize token account
    let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;

    // Validate owner
    if &token_account.owner != expected_owner {
        return Err(ProgramError::InvalidAccountData);
    }

    // Validate mint
    if &token_account.mint != expected_mint {
        return Err(ProgramError::InvalidAccountData);
    }

    Ok(token_account)
}

Low-Level Security Patterns

Account Reloading After External Calls

Vulnerable:

pub fn vulnerable_pattern(accounts: &[AccountInfo]) -> ProgramResult {
    let account = next_account_info(&mut accounts.iter())?;

    let balance_before = account.lamports();

    // External CPI call
    invoke(&some_instruction, &[account.clone()])?;

    // Account data not reloaded - still using stale reference!
    let balance_after = account.lamports();

    Ok(())
}

Secure:

pub fn secure_pattern(accounts: &[AccountInfo]) -> ProgramResult {
    let account = next_account_info(&mut accounts.iter())?;

    let balance_before = account.lamports();

    // External CPI call
    invoke(&some_instruction, &[account.clone()])?;

    // AccountInfo automatically reflects changes - lamports(), data, etc.
    // are fresh after CPI
    let balance_after = account.lamports();

    // But if you cached deserialized data, you must reload:
    let fresh_data = MyData::from_account_info(account)?;

    Ok(())
}

Clock and Timestamp Validation

Secure Pattern:

use solana_program::clock::Clock;
use solana_program::sysvar::Sysvar;

pub fn time_locked_operation(
    accounts: &[AccountInfo],
    unlock_timestamp: i64,
) -> ProgramResult {
    // Get clock sysvar
    let clock = Clock::get()?;

    // Validate unlock time has passed
    if clock.unix_timestamp < unlock_timestamp {
        return Err(ProgramError::InvalidArgument);
    }

    // Proceed with operation
    Ok(())
}

Native Rust Best Practices

Account Iteration Patterns

Best Practice:

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    let authority = next_account_info(account_info_iter)?;
    let config = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    // Validate all expected accounts consumed
    if account_info_iter.next().is_some() {
        return Err(ProgramError::InvalidAccountData);
    }

    // Validate accounts
    AccountValidation::new(authority).signer()?;
    AccountValidation::new(config)
        .owner(program_id)?
        .writable()?;

    Ok(())
}

State Management Patterns

Best Practice:

#[derive(BorshSerialize, BorshDeserialize)]
pub struct ProgramState {
    pub version: u8,
    pub is_initialized: bool,
    pub authority: Pubkey,
    // Add new fields at the end for upgradability
    pub feature_flags: u64,
}

impl ProgramState {
    pub const CURRENT_VERSION: u8 = 1;

    pub fn initialize(authority: Pubkey) -> Self {
        Self {
            version: Self::CURRENT_VERSION,
            is_initialized: true,
            authority,
            feature_flags: 0,
        }
    }

    pub fn validate(&self) -> ProgramResult {
        if !self.is_initialized {
            return Err(ProgramError::UninitializedAccount);
        }

        if self.version != Self::CURRENT_VERSION {
            return Err(ProgramError::InvalidAccountData);
        }

        Ok(())
    }
}

Security.txt Integration

Best Practice:

#[cfg(not(feature = "no-entrypoint"))]
solana_security_txt::security_txt! {
    name: "My Solana Program",
    project_url: "https://github.com/myorg/myprogram",
    contacts: "email:security@myorg.com,discord:myorg",
    policy: "https://github.com/myorg/myprogram/blob/main/SECURITY.md",
    preferred_languages: "en",
    source_code: "https://github.com/myorg/myprogram",
    auditors: "Auditor1, Auditor2"
}

Summary

Native Rust Solana programs require meticulous manual validation of all security properties:

  1. Always validate: signer, owner, writable, key equality
  2. Use discriminators to prevent account type confusion
  3. Store canonical bumps and validate PDA derivation
  4. Validate CPI targets and propagate account flags correctly
  5. Validate sizes before deserialization
  6. Check rent exemption for all accounts
  7. Use Result types - never unwrap or expect
  8. Validate token accounts completely before use
  9. Reload account data after external calls if cached
  10. Version your state and validate initialization

For each pattern, create reusable validation functions and leverage Rust's type system to enforce security invariants at compile time where possible.