Skip to content

Instantly share code, notes, and snippets.

@zfedoran
Last active April 16, 2025 19:56
Show Gist options
  • Save zfedoran/9130d71aa7e23f4437180fd4ad6adc8f to your computer and use it in GitHub Desktop.
Save zfedoran/9130d71aa7e23f4437180fd4ad6adc8f to your computer and use it in GitHub Desktop.
Solana Program Vulnerabilities Guide

Solana Program Vulnerabilities Guide

This guide outlines common vulnerabilities in Solana programs, focusing on Rust and Anchor-specific issues. Each vulnerability includes a description, insecure code patterns to detect, and mitigation strategies to enforce. Intended for intermediate to advanced developers familiar with Solana’s programming model.

1. Account Data Matching

Description: Failure to validate that an account’s data matches expected values, allowing malicious accounts to be processed.

Insecure Pattern:

pub fn update_admin_settings(ctx: Context<UpdateAdminSettings>, new_settings: AdminSettings) -> Result<()> {
    ctx.accounts.config_data.settings = new_settings;
    Ok(())
}

Mitigation:

  • Check account keys against expected values.
  • Use Anchor’s has_one or constraint attributes.
if ctx.accounts.admin.key() != ctx.accounts.config_data.admin {
    return Err(ProgramError::Unauthorized);
}
#[account(constraint = config_data.admin == admin.key())]

2. Account Data Reallocation

Description: Improper use of Anchor’s realloc function can waste compute units or expose stale data.

Insecure Pattern:

ctx.accounts.todo_list_data.realloc(required_data_len, false)?;

Mitigation:

  • Set zero_init = true after size decrease in the same transaction.
  • Use Address Lookup Tables (ALTs) for dynamic account interactions.
ctx.accounts.todo_list_data.realloc(required_data_len, true)?;

3. Account Reloading

Description: Failure to reload deserialized accounts after a CPI, leading to stale data usage.

Insecure Pattern:

rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;
msg!("Rewards: {}", ctx.accounts.staking_account.rewards);

Mitigation:

  • Use Anchor’s reload method post-CPI.
ctx.accounts.staking_account.reload()?;

4. Arbitrary CPI

Description: Invoking a program without verifying its identity, allowing malicious program execution.

Insecure Pattern:

invoke(&instruction, &[ctx.accounts.reward_account.clone(), ctx.accounts.ledger_program.clone()])?;

Mitigation:

  • Verify the target program’s ID.
  • Use Anchor’s CPI module.
if ctx.accounts.ledger_program.key() != &custom_ledger_program::ID {
    return Err(ProgramError::IncorrectProgramId);
}

5. Authority Transfer Functionality

Description: Lack of a mechanism to transfer authority, risking lockout or compromise.

Insecure Pattern:

require_keys_eq!(ctx.accounts.current_admin.key(), ctx.accounts.global_admin.authority);

Mitigation:

  • Implement a two-step nominate-and-accept process.
state.pending_authority = Some(new_authority);
require_keys_eq!(Some(ctx.accounts.new_authority.key()), state.pending_authority);
state.authority = ctx.accounts.new_authority.key();

6. Bump Seed Canonicalization

Description: Using non-canonical bump seeds for PDAs, allowing malicious PDA manipulation.

Insecure Pattern:

Pubkey::create_program_address(seeds, &ctx.program_id)?;

Mitigation:

  • Use find_program_address for canonical bumps.
  • Store and validate bump in account.
let (derived_address, bump) = Pubkey::find_program_address(seeds, &ctx.program_id);
profile_pda.bump = bump;

7. Closing Accounts

Description: Improper account closure, allowing reinitialization or misuse.

Insecure Pattern:

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

Mitigation:

  • Zero out data, set CLOSED_ACCOUNT_DISCRIMINATOR, and transfer lamports.
  • Use Anchor’s #[account(close = destination)].
let mut data = account.try_borrow_mut_data()?;
for byte in data.deref_mut().iter_mut() { *byte = 0; }
cursor.write_all(&CLOSED_ACCOUNT_DISCRIMINATOR)?;

8. Duplicate Mutable Accounts

Description: Same account passed multiple times as mutable, causing unintended mutations.

Insecure Pattern:

reward_account.balance += reward_amount;
bonus_account.balance += bonus_amount;

Mitigation:

  • Check for distinct account keys.
  • Use Anchor constraints.
if ctx.accounts.reward_account.key() == ctx.accounts.bonus_account.key() {
    return Err(ProgramError::InvalidArgument);
}
#[account(constraint = reward_account.key() != bonus_account.key())]

9. Frontrunning

Description: Malicious transaction ordering to manipulate values (e.g., price changes).

Insecure Pattern:

pub fn purchase_product(ctx: Context<PurchaseProduct>) -> Result<()> { ... }

Mitigation:

  • Include expected value checks.
assert!(ctx.accounts.product_listing.sale_price <= expected_price);

10. Insecure Initialization

Description: Initialization by unauthorized accounts, allowing attacker control.

Insecure Pattern:

ctx.accounts.central_state.authority = authority.key();

Mitigation:

  • Restrict to program’s upgrade authority.
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]

11. Loss of Precision

Description: Arithmetic errors from multiplication-after-division, saturating functions, or rounding.

Insecure Patterns:

(a / c) * b // Multiplication after division
transaction_amount.saturating_mul(reward_multiplier) // Saturating
Decimal::from(collateral_amount).try_div(self.0)?.try_round_u64() // Rounding

Mitigation:

  • Perform multiplication before division: (a * b) / c.
  • Use checked_* functions instead of saturating_*.
  • Use try_floor_u64 for rounding to avoid inflation.
match a.checked_mul(b).and_then(|res| res.checked_div(c)) {
    Some(result) => result,
    None => return Err(ProgramError::ArithmeticError),
}
Decimal::from(collateral_amount).try_div(self.0)?.try_floor_u64()

12. Missing Ownership Check

Description: Failure to verify account ownership, allowing unauthorized actions.

Insecure Pattern:

if config.admin != admin.pubkey() {
    return Err(ProgramError::InvalidAdminAccount);
}

Mitigation:

  • Check account owner matches expected program ID.
  • Use Anchor’s Account type or owner constraint.
if config.owner != program_id {
    return Err(ProgramError::InvalidConfigAccount);
}
#[account(owner = <expected_program_id>)]

13. Missing Signer Check

Description: Failure to verify transaction signer, allowing unauthorized execution.

Insecure Pattern:

if admin.pubkey() != config.admin {
    return Err(ProgramError::InvalidAdminAccount);
}

Mitigation:

  • Check is_signer field.
  • Use Anchor’s Signer type.
if !admin.is_signer {
    return Err(ProgramError::NotSigner);
}
pub admin: Signer<'info>,

14. Overflow and Underflow

Description: Silent integer overflows/underflows in release mode, altering values.

Insecure Pattern:

balance = balance - tokens_to_subtract;

Mitigation:

  • Enable overflow-checks = true in Cargo.toml.
  • Use checked_* functions or checked_math macro.
  • Safe casting with try_from.
match balance.checked_sub(tokens_to_subtract) {
    Some(new_balance) => new_balance,
    None => return Err(ProgramError::InsufficientFunds),
}
u32::try_from(amount).map_err(|_| ProgramError::InvalidArgument)

15. PDA Sharing

Description: Using the same PDA for multiple roles, enabling unauthorized access.

Insecure Pattern:

#[account(seeds = [b"staking_pool_pda"], bump)]

Mitigation:

  • Use unique seeds per functionality.
#[account(seeds = [b"staking_pool", &staking_pool.key().as_ref()], bump)]
#[account(seeds = [b"rewards_pool", &rewards_pool.key().as_ref()], bump)]

16. Remaining Accounts

Description: Unvalidated accounts in ctx.remaining_accounts, allowing malicious inputs.

Insecure Pattern:

for user_pda_info in ctx.remaining_accounts.iter() {
    // Process without validation
}

Mitigation:

  • Manually validate account ownership and data.
for user_pda_info in ctx.remaining_accounts.iter() {
    if user_pda_info.owner != expected_owner {
        return Err(ProgramError::InvalidAccountData);
    }
}

17. Rust-Specific Errors

Description: Unsafe code or panics introducing vulnerabilities.

Insecure Patterns:

unsafe { /* Dereference raw pointer */ }
array[index] // Out-of-bounds
option.unwrap() // None value

Mitigation:

  • Minimize unsafe usage, document, and audit.
  • Use safe array access (get), pattern matching for Option.
  • Validate inputs to prevent division by zero.
if let Some(value) = array.get(index) {
    // Process value
}
match option {
    Some(value) => value,
    None => return Err(ProgramError::InvalidData),
}

18. Seed Collisions

Description: Same PDA address for different seeds, causing conflicts.

Insecure Pattern:

#[account(seeds = [b"session", session_id.as_bytes()])]

Mitigation:

  • Use unique prefixes and identifiers in seeds.
  • Validate PDA uniqueness.
#[account(seeds = [b"session", session_id.as_bytes(), &organizer.key().as_ref()])]

19. Type Cosplay

Description: Misrepresenting account types due to missing discriminator checks.

Insecure Pattern:

let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();

Mitigation:

  • Check discriminator during deserialization.
  • Use Anchor’s Account type.
if user.discriminant != AccountDiscriminant::Admin {
    return Err(ProgramError::InvalidAccountData);
}
pub user: Account<'info, User>,

20. Precision Loss in Financial Math

Description: Inaccurate calculations due to floating-point usage or improper operation order, risking token loss or inflation.

Insecure Pattern:

let payout = win_pool / total_bets * bet_amount; // Division first
let result = Decimal::from(amount).try_round_u64(); // Rounding

Mitigation:

  • Use integers and minor units (e.g., lamports).
  • Multiply before dividing: (win_pool * bet_amount) / total_bets.
  • Use try_floor_u64 for rounding to avoid over-minting.
let payout = win_pool.checked_mul(bet_amount).and_then(|res| res.checked_div(total_bets))?;
Decimal::from(amount).try_floor_u64()

21. Inconsistent Rounding

Description: Variable rounding methods causing exploitable token loss or inflation.

Insecure Pattern:

let liquidity = Decimal::from(collateral).try_round_u64()?; // Inconsistent rounding

Mitigation:

  • Define a consistent rounding policy (e.g., round half up).
  • Use try_floor_u64 for collateral conversions.
let liquidity = Decimal::from(collateral).try_floor_u64()?;

22. Floating-Point in Interest Calculations

Description: Using floats for compound interest, leading to rounding errors.

Insecure Pattern:

let interest = principal * (1.0 + rate).powf(compounds * time); // Float-based

Mitigation:

  • Use spl-math::PreciseNumber or brine-fp::UnsignedNumeric for fixed-point arithmetic.
  • Calculate (principal * (1 + rate/compounds)^(compounds * time)).
let interest = PreciseNumber::from(principal).mul(PreciseNumber::from(1 + rate / compounds).pow(compounds * time))?;

23. Unhandled Overflows and Underflows

Description: Silent integer overflows/underflows in release mode, altering financial calculations.

Insecure Pattern:

let new_balance = balance + amount; // No overflow check

Mitigation:

  • Enable overflow-checks = true in Cargo.toml.
  • Use checked_* functions.
let new_balance = balance.checked_add(amount).ok_or(ProgramError::ArithmeticError)?;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment