Building a Solana Security Audit Toolkit: From Static Analysis to Exploit Development
Practical tools and techniques for professional Solana program auditing
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:
- Reconnaissance - Understand program architecture
- Static Analysis - Automated vulnerability scanning
- Manual Review - Deep dive into critical paths
- Dynamic Testing - Local program simulation
- Exploit Development - PoC for confirmed vulnerabilities
- 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:
| Account | Type Check | Owner Check | Signer | Writable | Constraints |
| vault | ✅ Account | ✅ Program | ❌ | ✅ | has_one = authority |
| user | ❌ AccountInfo | ❌ | ✅ | ❌ | - |
| authority | ✅ Signer | ❌ | ✅ | ❌ | - |
Red Flags:
AccountInfowithout 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
withdrawbe called inPausedstate? - Can
initbe called twice? - What happens if
closeis 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
| Severity | Criteria |
| Critical | Direct fund loss, unlimited scope |
| High | Fund loss with conditions, privilege escalation |
| Medium | Limited fund loss, DoS, data corruption |
| Low | Best practice violations, minor issues |
| Info | Suggestions, 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:
- Automated tools for broad coverage
- Manual review for deep understanding
- Dynamic testing for edge cases
- Exploit development for validation
- 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.