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.
Automated Security Tools
Automated tools catch a significant portion of common vulnerabilities and should be part of every development workflow.
| Tool | Type | What It Catches |
|---|
| Sec3 | Static analysis + audit platform | Account validation, privilege escalation, arithmetic issues |
| Soteria | Static analyzer | Missing owner checks, signer checks, overflow |
| Anchor's built-in checks | Framework validation | Account types, constraints (if using Anchor) |
| cargo-audit | Dependency scanner | Known vulnerabilities in Rust crate dependencies |
| Trident | Fuzz testing framework | Edge cases in program logic |
Sec3 (formerly Soteria) provides both automated analysis and professional audit services specifically for Solana programs. Their static analysis tool can be integrated into CI pipelines to catch issues before they reach production.
For fuzz testing, Trident generates random inputs and transactions to find edge cases that unit tests miss. Fuzzing is especially effective at finding arithmetic issues, state corruption bugs, and unexpected account combinations.
Manual Review Priorities
Automated tools are necessary but not sufficient. Manual review catches logic errors, economic exploits, and design flaws that static analysis cannot detect.
Focus areas for manual review:
- Permission model. Map every instruction to its required authorities. Verify that no instruction can be called by unauthorized users.
- Fund flows. Trace every path where tokens or SOL move. Ensure funds cannot be extracted by anyone other than the intended recipient.
- State transitions. Document the valid state machine for your protocol. Verify that no sequence of instructions can put the system into an invalid state.
- Economic invariants. Define the economic rules that must always hold (e.g., total deposits equals total withdrawals plus fees). Write tests that verify these invariants after every operation.
- Edge cases. Test with zero values, maximum values, duplicate accounts, and empty accounts. These boundary conditions often reveal bugs that normal-range testing misses.
Working with Audit Firms
For any protocol handling significant value, a professional audit is essential. Here is how to approach the process.
When to Audit
Audit after the code is feature-complete and internally reviewed, but before mainnet deployment. Auditing code that is still changing wastes time and money — auditors will review code that gets rewritten.