Since its 2020 launch, Solana has gained widespread recognition for its speed and scalability, rivaling Ethereum with a peak Total Value Locked (TVL) of $14 billion. Home to innovative dApps and a thriving NFT ecosystem, it has solidified its position as a leading blockchain.
However, Solana’s unique architecture also introduces distinct security challenges that developers must navigate to build robust applications. Solana's rapid ascent has not been without challenges—most notably, recurring network outages that raise concerns about its long-term stability. These disruptions stem from Solana’s unique consensus mechanism, which, while enabling high-speed transactions, also introduces centralization risks and vulnerabilities during peak congestion. The network relies on the Transaction Processing Unit (TPU) and optimistic concurrency, contributing to its impressive throughput but potentially exacerbating issues during network stress events.
This guide explores Solana-specific security risks, including common vulnerabilities in smart contracts, network stability issues, and best practices for securing Solana-based projects. Unlike Ethereum, where Solidity smart contracts dominate security discussions, Solana's security model requires developers to address both smart contract risks and lower-level architectural concerns. Understanding these challenges is crucial for ensuring the long-term safety and reliability of applications built on Solana.
Basic and Most Common Security Issues in Solana
Solana’s high-speed, low-cost transactions have made it a popular choice for developers building everything from meme coins to sophisticated decentralized applications (dApps). However, its unique architecture also introduces distinct security challenges that require careful attention. Unlike Ethereum, where smart contract audits primarily focus on Solidity-based vulnerabilities, Solana’s security risks often stem from both smart contract (program) design and backend integrations. Below are some of the most common security issues affecting Solana-based projects.
Security Concerns in Solana Programs
1. Missing Ownership Checks
Solana accounts are owned by smart contracts (programs), and ownership is specified in the account metadata. If a program does not properly validate the account owner, an attacker could substitute their own account, gaining unauthorized access to privileged functionality.
2. Account Confusion
Solana programs interact with multiple accounts for different functions. If a smart contract does not verify that an account contains the expected data type, an attacker could manipulate this oversight to exploit the system.
3. Missing Signer Checks
When executing certain instructions, it’s crucial to ensure that only authorized accounts can initiate them. In Solana, developers sometimes overlook verifying whether an account has signed the transaction (AccountInfo::is_signer), leaving the door open for unauthorized execution.
4. Integer Overflows & Underflows
Rust, Solana’s primary development language, prevents overflows in debug mode but defaults to two’s complement wrapping in release mode. If developers do not proactively manage integer boundaries, this can lead to unintended behavior, such as token supply miscalculations or balance inconsistencies.
5. Precision Loss in Arithmetic Operations
Incorrect handling of decimal precision can result in rounding errors, affecting token balances or financial calculations. Developers should use fixed-point arithmetic where possible to minimize these issues.
6. Arbitrary Cross-Program Invocation (CPI) Risks
Solana allows smart contracts to call other contracts, but failing to verify the target contract before invoking it can enable attackers to redirect transactions to malicious programs. Proper validation is essential to prevent unauthorized contract interactions.
7. Reentrancy Attacks
While Solana limits reentrant calls to simple recursion and a fixed depth of four, poorly structured programs can still be vulnerable to state manipulation through intermediate program calls. Developers must carefully manage execution flow to prevent unintended reentry.
8. Computational Unit (CU) Limits
Solana transactions consume computational units (CU), similar to Ethereum gas fees. Exceeding the 48 million CU limit can cause transactions to fail, which could be exploited to disrupt critical operations within a dApp.
9. Dependencies with Vulnerabilities
Using outdated or vulnerable dependencies is a common yet avoidable security risk. Developers should regularly audit their dependencies to ensure they are not introducing known exploits into their projects.
Securing Solana Projects: Solana Program Security
While Solana’s architecture enables high throughput, it also requires developers to be highly diligent in implementing security best practices. Conducting thorough security reviews, enforcing strict validation checks, and continuously monitoring for vulnerabilities can significantly reduce risks. As Solana continues to evolve, addressing these security concerns will be key to ensuring the network's reliability and trustworthiness.
Solana program security is not just about preventing hackers from stealing a project’s funds — it’s about ensuring a program behaves as intended, adhering to the project’s specifications and user expectations. Solana program security can affect a dApp's performance, scalability, and interoperability. Thus, developers must be aware of potential attack vectors and common vulnerabilities before building consumer-grade applications.
Solana’s Programming Model
Solana's programming model shapes the security landscape of applications built on its network. On Solana, accounts act as containers for data, similar to files on a computer. We can separate accounts into two general types: executable and non-executable. Executable accounts, or programs, are accounts capable of running code. Non-executable accounts are used for data storage without the ability to execute code (because they don't store any code). This decoupling of code and data means that programs are stateless — they interact with data stored in other accounts, passed by reference during transactions.
Solana is Attacker-Controlled
A transaction specifies the program to call, a list of accounts, and a byte array of instruction data. This model relies on the program to parse and interpret the accounts and instructions a given transaction provides. Allowing any account to be passed into a program's function grants attackers significant control over the data a program will operate on. Understanding Solana's inherently attacker-controlled programming model is crucial to developing secure programs.
Given an attacker's ability to pass any account into a program's function, data validation becomes a fundamental pillar of Solana program security. Developers must ensure that their program can distinguish between legitimate and malicious inputs. This includes verifying account ownership, ensuring accounts are of an expected type, and whether an account is a signer.
Why Does This Matter?
Unlike traditional smart contract platforms where access control is often enforced at the protocol level, Solana’s design allows any account to be passed into a program’s function. This means a malicious actor can inject unexpected or deceptive inputs, attempting to manipulate the program’s behavior. Without strict data validation and access control mechanisms, developers risk exposing their applications to severe vulnerabilities.
Key Risks in an Attacker-Controlled Model

Best Practices for Mitigating These Risks
To secure Solana programs from these attack vectors, developers should implement the following best practices:

By recognizing the attacker-controlled nature of Solana’s transaction model and proactively mitigating risks, developers can significantly enhance the security and robustness of their applications.
Examples of Security Vulnerabilities in Solana Development and Mitigation
Solana's high-speed blockchain infrastructure provides developers with a powerful platform to build decentralized applications (dApps). However, like any sophisticated system, Solana's smart contracts (or programs) are prone to various security vulnerabilities if not implemented carefully. Below, we explore key security concerns in Solana development, their risks, and recommended mitigations.
1. Account Data Matching
The Vulnerability
Account data matching vulnerabilities occur when developers fail to verify that an account's stored data matches expected values. This flaw is particularly dangerous in permission-related operations, where a program might unknowingly process requests from unauthorized accounts.
Example Scenario
A program allows administrators to update configuration settings. However, the implementation does not validate that the signer matches the stored administrator account:
pub fn update_admin_settings(
ctx: Context<UpdateAdminSettings>,
new_settings: AdminSettings
) -> Result<()> {
ctx.accounts.config_data.settings = new_settings;
Ok(())
}
#[derive(Accounts)]
pub struct UpdateAdminSettings<'info> {
#[account(mut)]
pub config_data: Account<'info, ConfigData>,
pub admin: Signer<'info>,
}
#[account]
pub struct ConfigData {
pub admin: Pubkey,
pub settings: AdminSettings,
}
Recommended Mitigation
To prevent unauthorized modifications, explicitly verify the administrator’s identity before applying changes:
pub fn update_admin_settings(
ctx: Context<UpdateAdminSettings>,
new_settings: AdminSettings,
) -> Result<()> {
if ctx.accounts.admin.key() != ctx.accounts.config_data.admin {
return Err(ProgramError::Unauthorized);
}
ctx.accounts.config_data.settings = new_settings;
Ok(())
}
Alternatively, use Anchor’s constraint attribute:
#[account(
mut,
constraint = config_data.admin == admin.key()
)]
2. Account Data Reallocation
The Vulnerability
In Anchor, the realloc function can introduce inefficiencies and expose stale data if not handled correctly. Reallocation is costly, and improper usage can lead to wasted compute units or unintentional exposure of prior data.
Example Scenario
A program dynamically modifies a to-do list, reallocating memory each time entries are added or removed:
pub fn modify_todo_list(
ctx: Context<ModifyTodoList>,
modifications: Vec<TodoModification>,
) -> ProgramResult {
let required_data_len = calculate_required_data_len(&modifications);
ctx.accounts.todo_list_data.realloc(required_data_len, false)?;
Ok(())
}
Recommended Mitigation
Use zero_init appropriately:
- Set zero_init to true when increasing size after reducing it in the same transaction to prevent stale data exposure.
- Set zero_init to false when increasing size without a prior decrease, as memory is already zeroed.
- Use Address Lookup Tables (ALTs) instead of frequent reallocation to optimize memory management.
3. Account Reloading
The Vulnerability
Anchor does not automatically refresh account states after a Cross-Program Invocation (CPI). If a program relies on data modified by a CPI without reloading the account, it may operate on outdated information.
Example Scenario
A staking program calculates rewards via a CPI but does not refresh the staking account afterward:
pub fn update_rewards(ctx: Context<UpdateStakingRewards>, amount: u64) -> Result<()> {
rewards_distribution::cpi::update_rewards(cpi_ctx, amount)?;
msg!("Rewards: {}", ctx.accounts.staking_account.rewards); // Uses stale data
Ok(())
}
Recommended Mitigation
Explicitly reload the account after a CPI:
ctx.accounts.staking_account.reload()?;
4. Arbitrary CPI
The Vulnerability
If a program allows arbitrary CPIs without verifying the target program's identity, an attacker could trick it into executing a call to a malicious program.
Example Scenario
A rewards distribution program calls an external ledger program without verifying its identity:
invoke(
&instruction,
&[
ctx.accounts.reward_account.clone(),
ctx.accounts.ledger_program.clone(),
],
)
Recommended Mitigation
Check the program ID before invoking:
if ctx.accounts.ledger_program.key() != &custom_ledger_program::ID {
return Err(ProgramError::IncorrectProgramId.into());
}
5. Authority Transfer Functionality
The Vulnerability
Programs often assign specific public keys as authorities for critical functions. However, without a secure way to transfer authority, a compromised or inactive admin key could cause irreparable issues.
Example Scenario
A program enforces an admin key but lacks the ability to transfer it:
require_keys_eq!(
ctx.accounts.current_admin.key(),
ctx.accounts.global_admin.authority,
);
Recommended Mitigation
Implement a two-step process for authority transfer:
1. Nominate a new authority:
pub fn nominate_new_authority(ctx: Context<NominateAuthority>, new_authority: Pubkey) -> Result<()> {
let state = &mut ctx.accounts.state;
require_keys_eq!(state.authority, ctx.accounts.current_authority.key());
state.pending_authority = Some(new_authority);
Ok(())
}
2. Accept authority transfer:
pub fn accept_authority(ctx: Context<AcceptAuthority>) -> Result<()> {
let state = &mut ctx.accounts.state;
require_keys_eq!(Some(ctx.accounts.new_authority.key()), state.pending_authority);
state.authority = ctx.accounts.new_authority.key();
state.pending_authority = None;
Ok(())
}
This ensures authority transfers are intentional and verifiable, reducing risks of accidental or malicious transfers.
6. Bump Seed Canonicalization
The Vulnerability
Failing to use the canonical bump when deriving Program Derived Addresses (PDAs) can lead to inconsistencies and potential attacks.
Example Scenario
A user-provided bump is used to derive a PDA:
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64, bump: u8) -> Result<()> {
let seeds: &[&[u8]] = &[b"profile", &user_id.to_le_bytes(), &[bump]];
let (derived_address, _) = Pubkey::create_program_address(seeds, &ctx.program_id)?;
if derived_address != ctx.accounts.profile.key() {
return Err(ProgramError::InvalidSeeds);
}
Ok(())
}
Recommended Mitigation
Using find_program_address ensures the canonical bump is used:
pub fn create_profile(ctx: Context<CreateProfile>, user_id: u64) -> Result<()> {
let seeds: &[&[u8]] = &[b"profile", user_id.to_le_bytes()];
let (derived_address, bump) = Pubkey::find_program_address(seeds, &ctx.program_id);
let profile_pda = &mut ctx.accounts.profile;
profile_pda.bump = bump;
Ok(())
}
7. Closing Accounts Securely
The Vulnerability
Improperly closed accounts can be reused for malicious purposes.
Example Scenario
A program transfers out an account’s lamports but does not properly close it:
pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
let account = ctx.accounts.data_account.to_account_info();
let destination = ctx.accounts.destination.to_account_info();
**destination.lamports.borrow_mut() += account.lamports();
**account.lamports.borrow_mut() = 0;
Ok(())
}
Recommended Mitigation
Zeroing out the account data and adding a closed account discriminator prevents misuse:
use anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR;
pub fn close_account(ctx: Context<CloseAccount>) -> ProgramResult {
let account = ctx.accounts.data_account.to_account_info();
let destination = ctx.accounts.destination.to_account_info();
**destination.lamports.borrow_mut() += account.lamports();
**account.lamports.borrow_mut() = 0;
let mut data = account.try_borrow_mut_data()?;
for byte in data.iter_mut() {
*byte = 0;
}
data[..8].copy_from_slice(&CLOSED_ACCOUNT_DISCRIMINATOR);
Ok(())
}
8. Duplicate Mutable Accounts
The Vulnerability
A malicious actor could pass the same account twice to an instruction expecting different accounts.
Example Scenario
A rewards program updates two accounts:
pub fn distribute_rewards(ctx: Context<DistributeRewards>, reward_amount: u64, bonus_amount: u64) -> Result<()> {
let reward_account = &mut ctx.accounts.reward_account;
let bonus_reward = &mut ctx.accounts.bonus_account;
reward_account.balance += reward_amount;
bonus_reward.balance += bonus_amount;
Ok(())
}
Recommended Mitigation
Ensure accounts are unique:
#[derive(Accounts)]
pub struct DistributeRewards<'info> {
#[account(mut, constraint = reward_account.key() != bonus_account.key())]
pub reward_account: Account<'info, RewardAccount>,
#[account(mut)]
pub bonus_account: Account<'info, RewardAccount>,
}
9. Frontrunning
The Vulnerability
A seller could frontrun a buyer's transaction by changing the sale price before the transaction executes.
Example Scenario
A seller updates a product’s sale price:
pub fn change_sale_price(ctx: Context<ChangeSalePrice>, new_price: u64) -> Result<()> {
ctx.accounts.product_listing.sale_price = new_price;
Ok(())
}
A buyer’s transaction does not check for price changes.
Recommended Mitigation
Include an expected price check in the purchase function:
10. Insecure Initialization
The Vulnerability
Unlike Ethereum’s EVM, where contracts are deployed with constructors that automatically set state variables, Solana programs require manual initialization through a function (often named initialize). If not secured properly, an attacker can frontrun the initialization function and take control of the program.
Example Scenario
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.central_state.authority = authority.key();
// ...
}
#[derive(Accounts)]
pub struct Initialize<'info> {
pub authority: Signer<'info>,
#[account(
mut,
init,
payer = authority,
space = CentralState::SIZE,
seeds = [b"central_state"],
bump
)]
pub central_state: Account<'info, CentralState>,
}
Recommended Mitigation
A safer approach ensures that only the program’s upgrade_authority can call the initialize function:
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>;
11. Loss of Precision
The Vulnerability
Precision loss in arithmetic operations can lead to incorrect calculations, arbitrage opportunities, and unintended program behavior. Solana programs should use fixed-point arithmetic instead of floating points due to the limited subset of float operations available in Rust.
Multiplication After Division
Performing division before multiplication can cause precision loss due to rounding errors. To mitigate this, always multiply first:
let result = (a * b) / c;
Saturating Arithmetic
Using saturating_* functions can lead to precision loss if the cap is unexpectedly reached:
pub fn calculate_reward(transaction_amount: u64, reward_multiplier: u64) -> u64 {
transaction_amount.saturating_mul(reward_multiplier)
}
Instead, implement logic to handle edge cases explicitly.
Rounding Errors
Rounding operations like try_round_u64() can artificially inflate values and lead to arbitrage attacks. Instead, use try_floor_u64() to round down:
Decimal::from(collateral_amount).try_div(self.0)?.try_floor_u64()
12. Missing Ownership Check
The Vulnerability
Ownership checks verify that a program owns an account before modifying it. Without this validation, unauthorized fund transfers or privileged operations can occur.
Example Scenario
pub fn admin_token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount);
}
}
Recommended Mitigation
Verify the ownership of the account:
if config.owner != program_id {
return Err(ProgramError::InvalidConfigAccount);
}
13. Missing Signer Check
The Vulnerability
Transactions require signatures from the sender’s private key for authentication. Without a signer check, any account can execute a transaction by supplying the correct parameters.
Example Scenario
if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}
Recommended Mitigation
Ensure that the signer flag is set:
if !admin.is_signer {
return Err(ProgramError::NotSigner);
}
14. Overflow and Underflow
The Vulnerability
Rust integers have fixed sizes. If a value exceeds its maximum or minimum limit, an overflow or underflow occurs. While Rust checks for these issues in debug mode, it does not in release mode, which Solana uses by default.
Example Scenario
let mut balance: u8 = account.data.borrow()[0];
balance = balance - tokens_to_subtract;
If tokens_to_subtract is greater than balance, an underflow occurs, increasing the balance instead of decreasing it.
Recommended Mitigation
Enable overflow checks in Cargo.toml:
[profile.release]
overflow-checks = true
Alternatively, use Rust’s checked arithmetic functions:
let balance = balance.checked_sub(tokens_to_subtract).ok_or(ProgramError::InvalidInstructionData)?;
15. Casting Vulnerabilities
The Vulnerability
Casting between integer types using the as keyword without proper checks can lead to integer overflow or underflow vulnerabilities. Rust truncates or extends values when converting between types, which can introduce unintended behavior. For example, when casting from u64 to u32, any value exceeding 4,294,967,295 will be truncated, potentially leading to unexpected results.
Recommended Mitigation
Use Rust's safe casting methods, such as try_from and from, to prevent these issues:
pub fn convert_token_amount(amount: u64) -> Result<u32, ProgramError> {
u32::try_from(amount).map_err(|_| ProgramError::InvalidArgument)
}
This ensures that if the value exceeds u32’s maximum limit, the program will return an error instead of producing an incorrect value.
16. PDA Sharing
The Vulnerability
Program Derived Addresses (PDAs) can introduce security risks if shared across multiple authority domains without proper access controls. Using a static seed to derive PDAs for different functionalities (e.g., staking and withdrawing rewards) can allow unauthorized access or fund manipulation.
Recommended Mitigation
Use distinct PDAs for different functionalities by incorporating unique seeds:
#[derive(Accounts)]
pub struct StakeTokens<'info> {
#[account(
mut,
seeds = [b"staking_pool", &staking_pool.key().as_ref()],
bump
)]
pub staking_pool: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct WithdrawRewards<'info> {
#[account(
mut,
seeds = [b"rewards_pool", &rewards_pool.key().as_ref()],
bump
)]
pub rewards_pool: AccountInfo<'info>,
}
This approach ensures PDAs are tied to specific operations, preventing unauthorized actions.
17. Remaining Accounts
The Vulnerability
The ctx.remaining_accounts parameter allows passing additional accounts dynamically but does not validate them. A malicious actor could exploit this by injecting unintended accounts, leading to unauthorized actions.
Recommended Mitigation
Manually validate each account passed through ctx.remaining_accounts by checking ownership and expected account structures before using them in program logic.
pub fn process(ctx: Context<SomeInstruction>) -> Result<()> {
for account in ctx.remaining_accounts.iter() {
require!(account.owner == EXPECTED_OWNER_PUBKEY, CustomError::InvalidAccountOwner);
}
Ok(())
}
18. Unsafe Rust
The Vulnerability
Rust’s unsafe keyword bypasses its memory safety guarantees, allowing direct memory access, calling unsafe functions, or modifying mutable static variables. Improper use can lead to memory corruption, undefined behavior, or security vulnerabilities.
Recommended Mitigation
Minimize the use of unsafe code and encapsulate it within safe abstractions. If unsafe is necessary, document it thoroughly and audit its implementation regularly.
unsafe fn critical_memory_operation(ptr: *mut u8) {
if !ptr.is_null() {
*ptr = 42; // Ensure safe memory handling
}
}
19. Panics and Error Management
The Vulnerability
A Rust panic occurs when an unrecoverable error happens. In Solana programs, panics can lead to unexpected behavior, stack unwinding, and even information leakage through error messages.
Recommended Mitigation
- Avoid operations that cause panics (e.g., division by zero, out-of-bounds array access).
- Use Result and Option for error handling instead of unwrap().
- Validate all inputs before executing logic that could fail unexpectedly.
pub fn safe_divide(numerator: u64, denominator: u64) -> Result<u64, ProgramError> {
if denominator == 0 {
return Err(ProgramError::InvalidArgument);
}
Ok(numerator / denominator)
}
20. Seed Collisions
The Vulnerability
PDAs are derived using seeds, and if two different inputs produce the same PDA, unintended behaviors such as fund loss or denial of service can occur.
Recommended Mitigation
- Use unique prefixes for seeds across different PDAs.
- Include unique identifiers (e.g., timestamps, user IDs) to avoid predictable seed reuse.
- Programmatically check for existing PDAs to prevent accidental overwrites.
let (pda, bump) = Pubkey::find_program_address(&[b"unique_seed", user.key().as_ref()], program_id);
21. Type Cosplay
The Vulnerability
Type cosplay occurs when an account type is misrepresented as another due to missing type checks during deserialization. This can allow unauthorized actions or data corruption.
Recommended Mitigation
- Introduce a discriminator field in account structures to validate types before deserializing.
- Use Anchor’s Account<'info, T> wrapper, which ensures type safety by automatically verifying the account discriminator.
#[account]
pub struct User {
pub discriminator: u8,
pub authority: Pubkey,
}
By checking the discriminator, the program ensures that only valid account types are processed.
Conclusion
The importance of program security cannot be overstated. This article has traversed the spectrum of common vulnerabilities, from Rust-specific errors to the complexities of Anchor’s realloc method. The path to mastering each of these vulnerabilities, and program security in general, is ongoing and demands continuous learning, adaptation, and collaboration.
As developers, our dedication to security should cover a wide range of responsibilities that go far beyond protecting assets. It's about building and maintaining trust with our users, ensuring the integrity and reliability of our applications, and actively contributing to the overall growth and stability of the Solana ecosystem. This involves staying informed about the latest security threats and vulnerabilities, adhering to best practices in secure coding, and proactively participating in the community to share knowledge and collaborate on solutions. By taking these steps, we can help create a more secure and resilient Solana ecosystem that benefits everyone.
Finally, we should never underestimate the critical importance of comprehensive security reviews. These reviews should be conducted at every stage of the development process and tailored specifically for the blockchain platform you are utilizing. Whether you're building on Solana, Ethereum, or another blockchain, potential vulnerabilities can be introduced at any point. By integrating security reviews into each phase to battle-test your project– from initial design and coding to testing and deployment – you proactively identify and mitigate risks before they can be exploited.
Remember that blockchain security is unique and requires specialized knowledge. A one-size-fits-all approach won't suffice. Engage security professionals and researchers with proven industry expertise to ensure your project is robust and resilient against potential attacks.
Secure your protocol today
Building on Solana? Cantina is your go-to for comprehensive end-to-end security. Looking to secure your crypto protocol? Let's talk. We can have a full quote turned around for you within 24 hours, catered exactly to your project's needs.
__________________________________________________________________________________
This guide was created in collaboration with Zigtur, one of Cantina's top security researchers and Solana experts. With over 200 public findings and multiple first-place rankings in security competitions, Zigtur brings an unmatched level of expertise.