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.
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
orconstraint
attributes.
if ctx.accounts.admin.key() != ctx.accounts.config_data.admin {
return Err(ProgramError::Unauthorized);
}
#[account(constraint = config_data.admin == admin.key())]
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)?;
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()?;
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);
}
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();
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;
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)?;
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())]
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);
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()))]
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 ofsaturating_*
. - 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()
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 orowner
constraint.
if config.owner != program_id {
return Err(ProgramError::InvalidConfigAccount);
}
#[account(owner = <expected_program_id>)]
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>,
Description: Silent integer overflows/underflows in release mode, altering values.
Insecure Pattern:
balance = balance - tokens_to_subtract;
Mitigation:
- Enable
overflow-checks = true
inCargo.toml
. - Use
checked_*
functions orchecked_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)
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)]
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);
}
}
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 forOption
. - 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),
}
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()])]
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>,
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()
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()?;
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
orbrine-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))?;
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
inCargo.toml
. - Use
checked_*
functions.
let new_balance = balance.checked_add(amount).ok_or(ProgramError::ArithmeticError)?;