Security in Solana program development is not optional — it is the difference between a successful protocol and a headline-making exploit. The Solana runtime model is fundamentally different from EVM-based chains, which means Ethereum security knowledge does not directly transfer. Solana-specific vulnerabilities require Solana-specific awareness.
This guide provides a practical audit checklist for developers building on Solana, covering the most common vulnerability classes, tools for automated detection, manual review best practices, and guidance on working with professional audit firms.
Why Solana Security Is Different
Solana's account model creates a fundamentally different attack surface compared to Ethereum. On Ethereum, a smart contract owns its storage. On Solana, programs are stateless — they operate on accounts that are passed in as part of each transaction. This means the program must validate every account it receives, because an attacker can pass in arbitrary accounts.
This architectural difference is responsible for the majority of Solana-specific vulnerabilities. The program cannot assume anything about the accounts it receives unless it explicitly verifies ownership, type, initialization state, and data content.
Program-level security is only half the picture — many losses happen at the wallet level, where users approve malicious transactions. For the end-user side of this, see our guide to Solana wallet drainer protection.
The Solana Security Audit Checklist
1. Account Validation
This is the most critical category. Missing or incomplete account validation is the root cause of the majority of Solana exploits.
Owner checks. Every account passed to your program should have its owner verified. If your program expects a token account, verify it is owned by the Token Program. If it expects a PDA, verify the owner is your program.
// Always verify account ownership
if account.owner != &expected_program_id {
return Err(ProgramError::IncorrectProgramId);
}
Signer checks. If an instruction requires authority (transferring tokens, modifying state, withdrawing funds), verify that the authority account has signed the transaction. Missing signer checks allow anyone to execute privileged operations.
PDA validation. Program Derived Addresses must be derived from the expected seeds and bump. Do not trust that a PDA account is correct just because it is passed in — re-derive the address and compare.
let (expected_pda, bump) = Pubkey::find_program_address(
&[b"vault", user.key.as_ref()],
program_id,
);
if expected_pda != *vault_account.key {
return Err(ProgramError::InvalidAccountData);
}
Account type and initialization. Verify that accounts are of the expected type and are properly initialized. An uninitialized account could be used to bypass validation logic that assumes certain data fields exist.
2. Arithmetic Vulnerabilities
Solana programs are written in Rust, which provides compile-time overflow protection in debug mode but wraps silently in release mode. This creates a real risk.
Integer overflow and underflow. Use checked arithmetic (checked_add, checked_sub, checked_mul, checked_div) for all calculations involving user-controlled values. Never use standard arithmetic operators for financial calculations.
// Bad: wraps on overflow in release mode
let result = amount_a + amount_b;
// Good: returns error on overflow
let result = amount_a.checked_add(amount_b)
.ok_or(ProgramError::ArithmeticOverflow)?;
Precision loss. When dividing tokens or calculating shares, truncation can result in rounding errors that accumulate over time. Use a consistent rounding strategy and consider using fixed-point math libraries for complex calculations.
Casting errors. Casting between integer types (e.g., u64 to u32) can silently truncate. Always validate that values fit within the target type before casting, or use try_into() which returns an error on overflow.
3. Cross-Program Invocation (CPI) Safety
CPI allows your program to call other programs. This is powerful but introduces attack vectors.
Privilege escalation through CPI. When your program invokes another program via CPI, it can pass along signer privileges. Ensure you only pass signer seeds for PDAs that your program legitimately controls. Never allow user-supplied seeds to be used in CPI signer contexts.
Return data validation. If your program depends on return data from a CPI call, validate it. A malicious program could return unexpected data to manipulate your program's logic.
Re-entrancy. While Solana's runtime provides some re-entrancy protection (a program cannot invoke itself directly), indirect re-entrancy through intermediate programs is possible. Ensure state updates happen before CPI calls (checks-effects-interactions pattern), not after.
4. State Management Vulnerabilities
Uninitialized account reuse. If your program closes an account by setting its lamports to zero but does not zero out the data, the account data remains in memory for the rest of the transaction. Another instruction in the same transaction could read stale data from the closed account. Always zero out account data when closing.
// When closing an account, zero the data
let mut data = account.try_borrow_mut_data()?;
data.fill(0);
**account.try_borrow_mut_lamports()? = 0;
Account data type confusion. If your program uses multiple account types with different data layouts, ensure each instruction validates that the account contains the correct type of data. Use a discriminator (type tag) at the beginning of each account's data.
Stale data between instructions. In a transaction with multiple instructions, account data modified by one instruction is visible to subsequent instructions. Ensure your program handles this correctly, especially when closing and recreating accounts within the same transaction.
5. Token and SOL Handling
Token account authority. When transferring tokens on behalf of users, verify that the authority account matches the expected owner of the token account. A mismatch means someone is trying to spend tokens they do not own.
Decimal handling. SPL tokens have variable decimals (0-9). Always use the mint's decimal value when calculating amounts. Assuming 9 decimals (like SOL) for a token with 6 decimals creates a 1000x error.
Wrapped SOL edge cases. Wrapped SOL (native SOL in a token account) behaves differently from other SPL tokens. Sync operations, close behavior, and balance calculations have nuances that can catch developers off guard. Test wrapped SOL paths explicitly.
Rent-exempt minimum. When creating accounts, ensure they are funded above the rent-exempt minimum. When transferring SOL out of PDAs, ensure enough remains to maintain rent exemption unless you intend to close the account entirely.