Skip to main content

Command Palette

Search for a command to run...

Building a Solana Security Audit Toolkit: From Static Analysis to Exploit Development

Practical tools and techniques for professional Solana program auditing

Published
6 min read

Security auditing isn't just about reading code—it's about systematically breaking it. In this article, I'll share my complete Solana audit toolkit, from initial reconnaissance to proof-of-concept exploit development.

The Audit Workflow

My typical Solana audit follows this flow:

  1. Reconnaissance - Understand program architecture
  2. Static Analysis - Automated vulnerability scanning
  3. Manual Review - Deep dive into critical paths
  4. Dynamic Testing - Local program simulation
  5. Exploit Development - PoC for confirmed vulnerabilities
  6. Reporting - Clear, actionable findings

Essential Tools

1. Soteria - Static Analysis

Soteria is the closest thing to Slither for Solana. It catches common vulnerabilities automatically.

# Install Soteria
cargo install soteria-analyzer

# Run on your program
soteria analyze ./programs/my_program

What Soteria Catches:

  • Missing signer checks
  • Integer overflow potential
  • Unvalidated account ownership
  • Unsafe arithmetic patterns

Example Output:

[HIGH] Missing signer check at withdraw.rs:45
  └─ user account is not validated as signer
[MEDIUM] Potential overflow at math.rs:23  
  └─ unchecked multiplication of u64 values

2. Anchor Security Extensions

For Anchor programs, leverage built-in security features:

use anchor_lang::prelude::*;

#[derive(Accounts)]
pub struct SecureContext<'info> {
    // Automatic ownership check
    #[account(
        mut,
        has_one = authority @ CustomError::InvalidAuthority,
    )]
    pub vault: Account<'info, Vault>,

    // Explicit signer requirement
    pub authority: Signer<'info>,

    // Token account constraints
    #[account(
        mut,
        token::mint = vault.mint,
        token::authority = authority,
    )]
    pub user_tokens: Account<'info, TokenAccount>,
}

3. Trident - Fuzz Testing

Fuzz testing finds edge cases humans miss. Trident generates random inputs to stress-test your program.

# Install Trident
cargo install trident-cli

# Initialize fuzz tests
trident init

# Run fuzzer
trident fuzz run-hfuzz

Custom Fuzz Target Example:

use trident_client::fuzzing::*;

#[derive(Arbitrary)]
pub struct DepositFuzzInput {
    pub amount: u64,
    pub user_balance: u64,
}

fn fuzz_deposit(input: DepositFuzzInput) {
    // Setup test environment
    let ctx = setup_program_test();

    // Execute with fuzzed inputs
    let result = ctx.deposit(input.amount);

    // Invariant checks
    assert!(ctx.vault_balance() >= input.amount);
    assert!(ctx.user_balance() <= input.user_balance);
}

4. Bankrun - Fast Local Testing

Bankrun provides lightning-fast program simulation:

import { BankrunProvider } from "anchor-bankrun";
import { startAnchor } from "solana-bankrun";

describe("Security Tests", () => {
  let context;
  let provider;

  beforeAll(async () => {
    context = await startAnchor(".", [], []);
    provider = new BankrunProvider(context);
  });

  it("should reject unauthorized withdrawal", async () => {
    const attacker = Keypair.generate();

    try {
      await program.methods
        .withdraw(new BN(1000))
        .accounts({
          vault: vaultPDA,
          authority: attacker.publicKey,  // Wrong authority
        })
        .signers([attacker])
        .rpc();

      fail("Should have thrown");
    } catch (e) {
      expect(e.message).toContain("ConstraintHasOne");
    }
  });
});

Manual Audit Patterns

Pattern 1: Account Validation Matrix

For every instruction, I build a validation matrix:

AccountType CheckOwner CheckSignerWritableConstraints
vault✅ Account✅ Programhas_one = authority
user❌ AccountInfo-
authority✅ Signer-

Red Flags:

  • AccountInfo without manual validation
  • Missing signer for privileged operations
  • No ownership checks for critical accounts

Pattern 2: State Machine Analysis

Draw the program's state machine and look for illegal transitions:

[Uninitialized] --init--> [Active] --pause--> [Paused]
                              |                   |
                              +---withdraw---+    |
                              |              v    |
                              +------<-------+----+
                                      ^
                                      |
                               [Closed] <--close--

Questions to Ask:

  • Can withdraw be called in Paused state?
  • Can init be called twice?
  • What happens if close is called while funds remain?

Pattern 3: Privilege Escalation Paths

Trace how authority is established and transferred:

// Authority assignment points
initialize() -> sets vault.authority
transfer_authority() -> changes vault.authority  // AUDIT THIS
upgrade_program() -> changes program authority

// Questions:
// 1. Who can call transfer_authority?
// 2. Is there a time delay?
// 3. Can authority be set to zero address?

Exploit Development

Building PoCs

When I find a vulnerability, I always build a working exploit. Here's my template:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";

describe("Exploit PoC: Missing Signer Check", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.VulnerableProgram as Program;

  it("drains vault without authorization", async () => {
    // Setup: Create legitimate vault with funds
    const victim = Keypair.generate();
    const vaultPDA = await createVaultWithFunds(victim, 1000);

    // Attack: Withdraw without being the authority
    const attacker = Keypair.generate();

    const attackerBalanceBefore = await getBalance(attacker.publicKey);

    // This should fail but doesn't due to missing signer check
    await program.methods
      .withdraw(new anchor.BN(1000))
      .accounts({
        vault: vaultPDA,
        user: attacker.publicKey,
        authority: victim.publicKey,  // Victim's pubkey, not signature!
      })
      .signers([attacker])  // Attacker signs, victim doesn't
      .rpc();

    const attackerBalanceAfter = await getBalance(attacker.publicKey);

    // Verify exploit success
    expect(attackerBalanceAfter - attackerBalanceBefore).toEqual(1000);
    console.log("💀 Exploit successful: Drained", 1000, "tokens");
  });
});

Simulating Economic Attacks

For DeFi protocols, I simulate economic exploits:

describe("Economic Exploit: Oracle Manipulation", () => {
  it("profits from price manipulation", async () => {
    // 1. Take flash loan of 10M tokens
    const flashLoan = await takeFlashLoan(10_000_000);

    // 2. Dump tokens to manipulate oracle price
    await dumpTokens(flashLoan.amount);

    // 3. Borrow against manipulated collateral
    const borrowed = await borrowWithCollateral(100); // 100 tokens
    // Oracle thinks our 100 tokens are worth 10M!

    // 4. Repay flash loan
    await repayFlashLoan(flashLoan);

    // 5. Profit calculation
    const profit = borrowed.value - flashLoan.fee;
    console.log(`💰 Profit: ${profit}`);
  });
});

Audit Report Structure

A professional audit report should include:

1. Executive Summary

  • Total findings by severity
  • Overall security posture
  • Critical recommendations

2. Finding Details

For each finding:

## [HIGH] Missing Signer Check in Withdraw Function

### Description
The `withdraw` instruction does not verify that the `authority` 
account has actually signed the transaction.

### Impact
Any user can drain funds from any vault by specifying the 
vault's authority pubkey without providing its signature.

### Proof of Concept
[Link to PoC code]

### Recommendation
Change `authority: AccountInfo` to `authority: Signer<'info>`

### Status
- [ ] Fixed
- [ ] Acknowledged
- [ ] Disputed

3. Severity Classification

SeverityCriteria
CriticalDirect fund loss, unlimited scope
HighFund loss with conditions, privilege escalation
MediumLimited fund loss, DoS, data corruption
LowBest practice violations, minor issues
InfoSuggestions, optimizations

Continuous Security

Integrate security into the development pipeline:

# .github/workflows/security.yml
name: Security Checks

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Solana
        run: sh -c "$(curl -sSfL https://release.solana.com/v1.18.0/install)"

      - name: Static Analysis
        run: soteria analyze ./programs

      - name: Fuzz Tests
        run: |
          cargo install trident-cli
          trident fuzz run-hfuzz --iterations 10000

      - name: Security Tests
        run: anchor test -- --features security-tests

Conclusion

Effective Solana auditing combines:

  1. Automated tools for broad coverage
  2. Manual review for deep understanding
  3. Dynamic testing for edge cases
  4. Exploit development for validation
  5. Clear reporting for remediation

The goal isn't just finding bugs—it's making the protocol secure before attackers find them.

Resources:


Have questions about Solana security? Connect with me on Twitter @thedreamwork.

More from this blog

D

ohmygod

65 posts