Rust Solana

Security scanning for Solana programs written in Rust. Solana smart contracts (programs) are written in Rust, offering memory safety and performance....

Last updated: January 14, 2026

Rust/Solana Guide

Security scanning for Solana programs written in Rust.

Overview

Solana smart contracts (programs) are written in Rust, offering memory safety and performance. BlockSecOps provides specialized scanners for Solana-specific security issues.


Available Scanners

Scanner Focus Description
Cargo Audit Dependencies Known vulnerabilities in crates
Clippy Code quality Rust linting and best practices
Soteria Solana security Solana-specific vulnerability detection
X-ray Deep analysis Advanced security scanning

Solana Security Focus

Account Validation

Solana programs must validate all accounts:

// BAD: No account validation
pub fn process_transfer(accounts: &[AccountInfo]) -> ProgramResult {
    let source = &accounts[0];
    let destination = &accounts[1];
    // Transfer without validation...
}

// GOOD: Proper account validation
pub fn process_transfer(accounts: &[AccountInfo]) -> ProgramResult {
    let source = &accounts[0];
    let destination = &accounts[1];

    // Validate account ownership
    if source.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }

    // Validate signer
    if !source.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Now safe to proceed...
}

Signer Verification

Always verify required signers:

// Verify signer
if !authority.is_signer {
    msg!("Authority must sign");
    return Err(ProgramError::MissingRequiredSignature);
}

PDA Verification

Validate Program Derived Addresses:

// Verify PDA
let (expected_pda, bump) = Pubkey::find_program_address(
    &[b"vault", user.key.as_ref()],
    program_id
);

if vault.key != &expected_pda {
    return Err(ProgramError::InvalidSeeds);
}

Common Vulnerabilities

Missing Owner Check

// BAD: Attacker can pass any account
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let vault = &accounts[0];
    // No owner verification...
}

// GOOD: Verify program owns the account
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let vault = &accounts[0];

    if vault.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    // Safe to proceed...
}

Missing Signer Check

// BAD: Anyone can call this
pub fn admin_function(accounts: &[AccountInfo]) -> ProgramResult {
    let admin = &accounts[0];
    // No signer check...
}

// GOOD: Require signature
pub fn admin_function(accounts: &[AccountInfo]) -> ProgramResult {
    let admin = &accounts[0];

    if !admin.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
}

Integer Overflow

// BAD: Can overflow
let total = amount1 + amount2;

// GOOD: Checked arithmetic
let total = amount1.checked_add(amount2)
    .ok_or(ProgramError::ArithmeticOverflow)?;

Type Confusion

// BAD: No type validation
let account_data = Account::unpack(&account.data.borrow())?;

// GOOD: Validate discriminator/type
let account_data = Account::unpack(&account.data.borrow())?;
if account_data.account_type != AccountType::Vault {
    return Err(ProgramError::InvalidAccountData);
}

Anchor Framework

If using Anchor, additional validation is automatic:

use anchor_lang::prelude::*;

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

    pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
        // Anchor validates:
        // - Account ownership
        // - Signers (from #[account] attributes)
        // - Account types

        // Your logic here
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut, has_one = authority)]
    pub vault: Account<'info, Vault>,

    pub authority: Signer<'info>,  // Automatically verified

    #[account(mut)]
    pub destination: AccountInfo<'info>,
}

Project Setup

Directory Structure

program/
├── src/
│   ├── lib.rs
│   ├── processor.rs
│   ├── instruction.rs
│   ├── state.rs
│   └── error.rs
├── Cargo.toml
└── Anchor.toml  # If using Anchor

Cargo.toml

[package]
name = "my-program"
version = "0.1.0"
edition = "2021"

[dependencies]
solana-program = "1.16"
borsh = "0.10"
thiserror = "1.0"

[dev-dependencies]
solana-program-test = "1.16"
solana-sdk = "1.16"

What to Upload

Include:

  • src/ directory with all .rs files
  • Cargo.toml and Cargo.lock
  • Anchor.toml if applicable

Exclude:

  • target/ build directory
  • Test artifacts

Scanner Detection

Cargo Audit

Checks for known vulnerabilities in dependencies:

Crate:  borsh
Version: 0.9.0
Warning: RUSTSEC-2023-0001
Title:  Memory corruption vulnerability

Clippy

Finds code quality and potential bugs:

warning: this could be a `const fn`
  --> src/processor.rs:45:1
   |
45 | pub fn get_fee() -> u64 {
   | ^^^^^^^^^^^^^^^^^^^^^^^^

Soteria

Finds Solana-specific issues:

src/processor.rs:78 - Missing signer check
  The authority account is not verified as a signer
  Severity: Critical

src/processor.rs:92 - Missing owner check
  Account ownership not validated before deserialization
  Severity: High

X-ray

Deep security analysis:

src/processor.rs:105 - Integer overflow risk
  Unchecked arithmetic operation could overflow
  Severity: High
  Recommendation: Use checked_add/checked_sub

Best Practices

Account Validation Checklist

fn validate_accounts(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    let account = &accounts[0];

    // 1. Check owner
    if account.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }

    // 2. Check signer (if required)
    if !account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // 3. Check writable (if modifying)
    if !account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    // 4. Check data size
    if account.data_len() < EXPECTED_SIZE {
        return Err(ProgramError::InvalidAccountData);
    }

    // 5. Validate PDA if applicable
    let (expected_pda, _) = Pubkey::find_program_address(
        &[SEED],
        program_id
    );
    if account.key != &expected_pda {
        return Err(ProgramError::InvalidSeeds);
    }

    Ok(())
}

Safe Arithmetic

// Always use checked arithmetic
let result = a.checked_add(b)
    .ok_or(ProgramError::ArithmeticOverflow)?;

let result = a.checked_sub(b)
    .ok_or(ProgramError::InsufficientFunds)?;

let result = a.checked_mul(b)
    .ok_or(ProgramError::ArithmeticOverflow)?;

let result = a.checked_div(b)
    .ok_or(ProgramError::DivisionByZero)?;

Error Handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ProgramError {
    #[error("Invalid instruction")]
    InvalidInstruction,

    #[error("Missing required signature")]
    MissingSignature,

    #[error("Invalid account owner")]
    InvalidOwner,

    #[error("Arithmetic overflow")]
    Overflow,
}

Cross-Program Invocation (CPI) Security

// Validate CPI calls
pub fn safe_cpi(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
) -> ProgramResult {
    // Verify the program being called
    let target_program = &accounts[0];

    // Whitelist known programs
    let allowed_programs = [
        spl_token::id(),
        system_program::id(),
    ];

    if !allowed_programs.contains(target_program.key) {
        return Err(ProgramError::InvalidProgramId);
    }

    // Proceed with CPI...
    Ok(())
}

Next Steps