Solana's account model is fundamentally different from Ethereum's. Instead of contracts with internal storage, Solana programs operate on accounts passed as instruction parameters. This creates unique security challenges: missing account validation, PDA seed manipulation, CPI authority escalation, and arbitrary account access. If you're coming from Solidity, these bugs will surprise you.
1. Account Validation: The #1 Solana Vulnerability
Solana programs receive accounts as function parameters. The program must verify that each account is the correct one — attackers can pass any account.
// VULNERABLE: No owner check on the token account
pub fn withdraw(ctx: Context) -> Result<()> {
// Attacker passes their own token account as 'vault'
let vault = &ctx.accounts.vault;
transfer_tokens(vault, ctx.accounts.recipient, amount)?;
Ok(())
}
// SECURE with Anchor: Automatic validation
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(
mut,
seeds = [b"vault", authority.key().as_ref()],
bump,
token::mint = mint,
token::authority = vault_authority,
)]
pub vault: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub mint: Account<'info, Mint>,
/// CHECK: PDA authority
#[account(seeds = [b"authority"], bump)]
pub vault_authority: AccountInfo<'info>,
}
2. PDA Security
Program Derived Addresses (PDAs) are deterministic addresses owned by programs. Seeds must be carefully chosen to prevent collisions and manipulation.
- ✅ Include user-specific data in PDA seeds to prevent cross-user access
- ✅ Always verify bump seed to ensure canonical PDA
- ✅ Use unique seed prefixes for different account types
- ❌ Don't use predictable or user-controllable seeds without verification
3. Cross-Program Invocation (CPI) Risks
CPI lets one program call another. The danger: passing signer privileges to a malicious program, or calling the wrong program entirely.
// VULNERABLE: Not checking the program being invoked
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let cpi_program = next_account_info(accounts)?;
// Attacker passes a malicious program!
invoke(&instruction, &[account1, account2])?;
Ok(())
}
// SECURE: Verify program ID
require_keys_eq!(cpi_program.key(), spl_token::ID, MyError::InvalidProgram);
4. Signer Verification
Every instruction that modifies user state must verify the appropriate signer. In Anchor, use the Signer type. In native Solana, check account.is_signer.
5. Integer Overflow in Rust
Rust panics on overflow in debug mode but wraps silently in release mode. Solana programs compile in release mode, so overflow checks are needed.
// VULNERABLE in release builds
let total = amount_a + amount_b; // Can wrap silently
// SECURE
let total = amount_a.checked_add(amount_b)
.ok_or(MyError::Overflow)?;
Common Anchor Pitfalls
- Using
AccountInfoinstead ofAccount<'info, T>— bypasses deserialization checks - Missing
has_oneorconstraintin account validation structs - Forgetting to check
is_initializedon accounts - Not closing accounts properly — rent-exempt lamports remain
How Vultbase Audits Solana Programs
- cargo-audit — Checks for known vulnerabilities in Rust dependencies
- cargo-clippy — Catches common Rust antipatterns and potential bugs
- Semgrep — Custom rules for Solana-specific vulnerability patterns
- Pattern DB — 127 Solana-specific patterns covering account validation, PDA, CPI, and signer issues
Solana's account model is powerful but unforgiving. Submit your Solana program for a comprehensive security audit.