Feb 12, 2025

Securing Solana: Solana and Rust Security Basics (Part 1)

Revealing the basics of Solana and Rust security: Solana will flourish with the right security best practises

No, Solana is not only memes.

Solana is a colorful network with multiple use cases and so many users it becomes unfair to put them all under the same hood.

Following our most recent audit of Solana's new DEX Snapper by solana.fun, we're happy to see serious, smart and dedicated developers, devoted to improving the Solana user's experience.

Rust Smart Contract Security

One thing Solana users and Ethereum users have in common is that they don't want their money stolen.

A way to stay safe is to only interact with verified and audited protocols and inform yourself of the known risks and vulnerabilities.

Rust is a systems programming language that stands out for its unique ownership model and memory safety guarantees without using garbage collection. Unlike Solidity, which is specifically designed for Ethereum smart contracts and runs in a virtual machine, Rust is a general-purpose language that compiles directly to machine code. What makes Rust particularly important is its "zero-cost abstractions" and compile-time checks that prevent common programming errors like null pointer dereferencing and data races, without sacrificing performance.

To use Rust safely, developers should embrace its ownership system by understanding borrowing rules and leveraging the compiler's strict checks rather than fighting against them. The language's emphasis on memory safety makes it ideal for systems programming, WebAssembly development, and building high-performance applications where reliability is crucial.

Common Solana Smart Contract Vulnerabilities


Data Matching

Solana programs must rigorously validate all input accounts since attackers can pass any account into program functions. Security relies on verifying account ownership, type, and signer status to distinguish legitimate from malicious inputs.

When developers fail to check if the data stored on an account matches an expected set of values, a program may inadvertently operate with incorrect or maliciously substituted accounts. This vulnerability is particularly acute in scenarios involving permission-related checks.

Here's an example:

use anchor_lang::prelude::*;

#[program]
pub mod vulnerable_program {
    use super::*;

    // Vulnerable function - lacks proper validation
    pub fn transfer_tokens(
        ctx: Context<Transfer>,
        amount: u64
    ) -> Result<()> {
        // INCORRECT: No validation of authority
        let from = &ctx.accounts.from;
        let to = &ctx.accounts.to;
        
        // Transfer tokens without checking ownership or permissions
        **from.try_borrow_mut_lamports()? -= amount;
        **to.try_borrow_mut_lamports()? += amount;
        
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub from: AccountInfo<'info>,
    #[account(mut)]
    pub to: AccountInfo<'info>,
    pub authority: Signer<'info>,
}

Recommended secure version:

#[derive(Accounts)]
pub struct SecureTransfer<'info> {
    #[account(
        mut,
        constraint = from.owner == program_id,
        constraint = from.key() == authority.key()
    )]
    pub from: AccountInfo<'info>,
    #[account(mut)]
    pub to: AccountInfo<'info>,
    pub authority: Signer<'info>,
}


Loss of Precision

Multiplication After Division

A classic example of precision loss occurs when performing multiplication after division, which can yield different results from performing multiplication before division. For example, consider the following expressions: (a / c) * b and (a * b) / c. Mathematically, these expressions are associative - they should yield the same result. However, in the context of Solana and fixed-point arithmetic, the order of operations matters significantly. Performing division first (a / c) may result in a loss of precision if the quotient is rounded down before it's multiplied by b. This could result in a smaller result than expected.

Rounding Errors

Rounding operations are a common loss of precision. Pay attention to the direction in which the rounding happens. The choice of rounding method can significantly impact the accuracy of calculations and the behaviour of the whole system.

☝️ Rounding up can be dangerous as it can artificially inflate values, leading to discrepancies between the actual and expected calculations. In some cases it can lead to issuing more liquidity tokens than the collateral amount justifies.

👇 Rounding down can be risky for example in deposit functions, as it favours the depositors, not the protocol owners. What is more, in some cases rounding down can also be used to mint small LP shares for free, thereby stealing funds from other liquidity providers.

Want to see an example scenario in which rounding down is problematic? Refer to our latest audit report's first High vulnerability.

Best practises with rounding in Solana:

1. Use fixed-point libraries or integer arithmetic to represent monetary amounts.

2. When floating point operations are required, minimize the number of rounding operations.

BUT! This might not be enough.. Complex code requires crafty, one-of-a-kind solutions, such as setting specific thresholds for rounding decisions or applying different logic based on the size of the values involved 🪄


Closing Accounts

We've seen many instances where accounts not closed properly are causing issues to a Solana protocol.

Such accounts can be reinitialized or misused leading to unauthorized action or access within the program.

Here's an example:

// VULNERABLE VERSION - Account can be reinitialized
#[program]
pub mod vulnerable_program {
    use super::*;

    pub fn close_vault(ctx: Context<CloseVault>) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        let authority = &ctx.accounts.authority;
        
        // Transfer remaining balance
        let vault_lamports = **vault.to_account_info().try_borrow_lamports()?;
        **vault.to_account_info().try_borrow_mut_lamports()? -= vault_lamports;
        **authority.try_borrow_mut_lamports()? += vault_lamports;
        
        // VULNERABLE: Account data isn't properly zeroed
        // The account can be reinitialized and reused
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CloseVault<'info> {
    #[account(mut)]
    pub vault: Account<'info, VaultState>,
    #[account(mut)]
    pub authority: Signer<'info>,
}

Here's the secure version with proper account closure:

// SECURE VERSION
#[program]
pub mod secure_program {
    use super::*;

    pub fn close_vault(ctx: Context<CloseVault>) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        let authority = &ctx.accounts.authority;
        
        // Transfer remaining balance
        let vault_lamports = **vault.to_account_info().try_borrow_lamports()?;
        **vault.to_account_info().try_borrow_mut_lamports()? -= vault_lamports;
        **authority.try_borrow_mut_lamports()? += vault_lamports;
        
        // Zero out the account's data
        let mut data = vault.to_account_info().try_borrow_mut_data()?;
        for byte in data.iter_mut() {
            *byte = 0;
        }
        
        Ok(())
    }
}

#[derive(Accounts)]
pub struct CloseVault<'info> {
    #[account(
        mut,
        close = authority,  // This ensures the account is properly closed
        constraint = vault.authority == authority.key() // Verify authority
    )]
    pub vault: Account<'info, VaultState>,
    #[account(mut)]
    pub authority: Signer<'info>,
}


Front-running

Yes, Solana has different architecture than Ethereum, with Proof of History (PoH) and a single leader to sequence transactions making front-running a tad more difficult to achieve.

However, it's definitely not impossible. Observers of bundled transactions in a mempool can still see pending transactions, insert their own transactions ahead of others and manipulate transaction ordering within a block.

To make sure there is no front-running possibilities in your protocol, make sure to think of:

  • Time-bound validity window

  • Price slippage protection

  • Minimum block aging

  • Owner authorization

Here is a safe implementation:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::clock::Clock;

#[program]
pub mod secure_trading {
    use super::*;

    pub fn create_order(
        ctx: Context<CreateOrder>,
        min_price: u64,
        max_price: u64,
        expiry_timestamp: i64,
        amount: u64
    ) -> Result<()> {
        let clock = Clock::get()?;
        let current_timestamp = clock.unix_timestamp;

        // Ensure order hasn't expired
        require!(
            current_timestamp < expiry_timestamp,
            ErrorCode::OrderExpired
        );

        let order = &mut ctx.accounts.order;
        order.owner = ctx.accounts.owner.key();
        order.min_price = min_price;
        order.max_price = max_price;
        order.expiry_timestamp = expiry_timestamp;
        order.amount = amount;
        order.created_at = current_timestamp;

        Ok(())
    }

    pub fn execute_order(
        ctx: Context<ExecuteOrder>,
        execution_price: u64
    ) -> Result<()> {
        let order = &ctx.accounts.order;
        let clock = Clock::get()?;
        
        // Check if order is still valid
        require!(
            clock.unix_timestamp < order.expiry_timestamp,
            ErrorCode::OrderExpired
        );

        // Verify price is within acceptable range
        require!(
            execution_price >= order.min_price && execution_price <= order.max_price,
            ErrorCode::PriceOutOfRange
        );

        // Optional: Add minimum blocks that must pass
        require!(
            clock.slot >= order.created_at + 10, // minimum 10 blocks
            ErrorCode::OrderTooFresh
        );

        // Execute trade logic here
        // ...

        Ok(())
    }
}

#[derive(Accounts)]
pub struct CreateOrder<'info> {
    #[account(
        init,
        payer = owner,
        space = 8 + OrderState::SIZE
    )]
    pub order: Account<'info, OrderState>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct ExecuteOrder<'info> {
    #[account(
        mut,
        constraint = order.owner == owner.key(),
        close = owner
    )]
    pub order: Account<'info, OrderState>,
    pub owner: Signer<'info>,
}

#[account]
pub struct OrderState {
    pub owner: Pubkey,
    pub min_price: u64,
    pub max_price: u64,
    pub expiry_timestamp: i64,
    pub amount: u64,
    pub created_at: i64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Order has expired")]
    OrderExpired,
    #[msg("Execution price out of acceptable range")]
    PriceOutOfRange,
    #[msg("Order must age minimum number of blocks")]
    OrderTooFresh,
}

Solana Security

While this article covered several critical vulnerabilities, we've only scratched the surface of what malicious actors might exploit. In Part 2, we'll dive deeper into advanced exploitation techniques, including Type Cosplay vulnerabilities and PDA manipulation vulnerabilities.

Don't wait for a security incident to prioritize your program's security - reach out to us for a comprehensive audit. With our track record of securing major Solana protocols and uncovering novel attack vectors, we can help ensure your program launches with the security it deserves. Your users trust you with their assets; let us help you protect them.

STAY SAFU

Audita's Team

Tell us about your project

Tell us about your project

Tell us about your project

Blog

More from Audita

Our take on Web3 security

Our CLIENTS

Testimonials