Solana Scanner Skill
SkillSecurityUse when auditing Solana programs for security vulnerabilities, reviewing Anchor or Pinocchio/native Rust smart contracts, checking CPI safety, PDA validation, account ownership, signer verification, or Token-2022 security.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Solana Scanner Skill skill
What this skill tells your AI
The instructions your AI receives, as published by 0x-shashi/web3-audit-skills in skills/solana-scanner/SKILL.md and read by ahel’s review.
Purpose
Analyze Solana programs (Rust/Anchor) for security vulnerabilities specific to the Solana runtime model. Solana's account-based execution model, where programs are stateless and all state is passed via accounts, creates a fundamentally different attack surface from EVM chains.
Core Principle
An attacker controls every account, argument, ordering, and CPI call-graph passed to your program. Your on-chain code must prove each input is legitimate before touching state or funds.
Solana Security Model
| Property | Solana | EVM |
|---|---|---|
| Execution model | Programs receive accounts as input | Contracts own their storage |
| State ownership | Account owner (program ID) controls data | Contract controls its own storage |
| Caller identity | Signer flag on accounts | msg.sender |
| Cross-program calls | CPI — accounts passed through | Internal calls share storage |
| Math safety | Overflow wraps in release mode | Solidity 0.8+ reverts on overflow |
| Account validation | Manual (native) or declarative (Anchor) | Automatic via msg.sender |
| Upgrades | Program authority can upgrade any time | Proxy patterns required |
| Rent | Accounts must maintain minimum SOL balance | No rent (storage is permanent) |
Detection Capabilities
Critical — Direct Fund Loss
| Vulnerability | Description | Detection Signal |
|---|---|---|
| Missing signer check | Privileged instruction lacks signer validation | AccountInfo without is_signer check |
| Missing owner check | Program accepts accounts owned by other programs | No owner == program_id validation |
| Arbitrary CPI | Cross-program invocation to user-controlled program ID | invoke() with unchecked program_id |
| PDA seed manipulation | PDA derived with controllable seeds | Seeds include user-controlled data without validation |
| Account data overwrite | Writing data to wrong account | Missing discriminator / account type check |
High — Significant Impact
| Vulnerability | Description | Detection Signal |
|---|---|---|
| Integer overflow (release) | Wrapping arithmetic in release builds | Math ops without checked_* or Anchor require! |
| Account closing revival | Closed account can be revived in same tx | Close without zeroing data + relying on zero lamports |
| Duplicate account injection | Same account passed for two different parameters | No uniqueness check between accounts |
| Type confusion | Account deserialized as wrong type | Missing discriminator validation |
| CPI privilege escalation | CPI inherits signer privileges incorrectly | invoke_signed() with wrong seeds |
Medium — Conditional Impact
| Vulnerability | Description | Detection Signal |
|---|---|---|
| PDA bump seed guessing | Not storing/reusing canonical bump | find_program_address in instruction logic |
| Missing rent exemption | Account may be garbage collected | No rent-exempt check after creation |
| Unchecked account size | Account realloc without bounds | realloc() without size validation |
| Clock dependency | Using Clock::get() for security-sensitive logic | Validator can influence timestamp slightly |
| Token account validation | SPL Token account not validated for mint/owner | Missing token::mint or token::authority check |
Key Audit Patterns
Vulnerability Categories with Prevention Code
Each category shows the risk, an attack example, and prevention for both Anchor and Pinocchio/native Rust.
1. Missing Owner Checks
Risk: Attacker substitutes a fake account owned by their own program with crafted data.
// Anchor Prevention — Account<'info, T> auto-checks owner
#[account]
pub struct Vault { pub authority: Pubkey, pub balance: u64 }
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>, // owner = this program ✓
pub authority: Signer<'info>,
}
// Pinocchio Prevention — explicit owner check
assert!(vault.is_owned_by(&crate::ID), ProgramError::IllegalOwner);
2. Missing Signer Checks
Risk: Anyone can call privileged instructions (withdraw, admin config).
// Anchor: Signer<'info> enforces is_signer automatically
pub authority: Signer<'info>,
// Pinocchio: manual check
assert!(authority.is_signer(), ProgramError::MissingRequiredSignature);
3. Arbitrary CPI
Risk: Attacker passes a malicious program ID, redirecting CPI to their program.
// Anchor Prevention — Program<'info, T> validates executable + ID
pub token_program: Program<'info, Token>,
// Pinocchio Prevention — compare key against known constant
assert_eq!(token_program.key(), &spl_token::ID);
4. Reinitialization
Risk: Attacker re-calls initialize to overwrite authority/settings.
// Anchor: init constraint creates + sets discriminator in one atomic step
#[account(init, payer = user, space = 8 + Vault::INIT_SPACE)]
pub vault: Account<'info, Vault>,
// ⚠️ Avoid init_if_needed — lets attacker front-run with bad data
// Pinocchio: check discriminator before writing
let data = vault.try_borrow_data()?;
assert_eq!(data[0..8], [0u8; 8], "already initialized");
5. PDA Sharing
Risk: Multiple users share one PDA → state confusion, fund theft.
// ❌ Bad: seeds only use pool-level data
#[account(seeds = [b"pool", pool.mint.as_ref()], bump)]
// ✅ Good: include user-specific identifier
#[account(seeds = [b"position", pool.key().as_ref(), user.key().as_ref()], bump)]
6. Type Cosplay
Risk: Attacker passes account of type A where type B is expected (same size).
// Anchor auto-adds 8-byte discriminator to all #[account] types,
// and Account<'info, T> validates it on deserialization.
// Pinocchio: first 8 bytes must be a unique discriminator per type
let disc = &data[0..8];
assert_eq!(disc, VAULT_DISCRIMINATOR, "wrong account type");
7. Duplicate Mutable Accounts
Risk: Same account passed as both source and destination → double-counting.
// Both frameworks — explicit key comparison
require!(source.key() != destination.key(), ErrorCode::DuplicateAccounts);
8. Account Revival (Closing)
Risk: Closed account (0 lamports) data remains in tx — later instruction reads stale state.
// Anchor: close transfers lamports + zero discriminator
#[account(mut, close = destination)]
pub vault: Account<'info, Vault>,
// Pinocchio: manually zero + transfer + realloc
data[0] = 0xFF; // poison discriminator
**vault.try_borrow_mut_lamports()? = 0;
**destination.try_borrow_mut_lamports()? += lamports;
vault.realloc(0, false)?;
9. Data Matching
Risk: Token account's mint or authority doesn't match expected values.
// Anchor: has_one validates field matches account key
#[account(mut, has_one = authority, has_one = mint)]
pub token_acct: Account<'info, TokenAccount>,
// Pinocchio: manual comparison
assert_eq!(token_acct.authority, expected_authority.key());
Account Validation Matrix
Every instruction must validate EVERY account it accesses. Use this matrix:
| Check | Native Solana | Anchor |
|---|---|---|
| Is signer? | account.is_signer | Signer<'info> |
| Is writable? | account.is_writable | #[account(mut)] |
| Correct owner? | account.owner == &program_id | Account<'info, T> (automatic) |
| Correct PDA? | Pubkey::find_program_address() | #[account(seeds = [...], bump)] |
| Correct data type? | Check discriminator manually | Account<'info, T> (automatic) |
| Belongs to user? | Manual field comparison | #[account(has_one = owner)] |
| Not closed? | Check lamports > 0 and data | Account<'info, T> (checks discriminator) |
| Unique? | Compare pubkeys between params | Manual (Anchor doesn't auto-check) |
Solana-Specific Attack Vectors
- Account Confusion Attack: Passing a token account where a mint account is expected (or vice versa)
- Reinitialization Attack: Calling
initializeon already-initialized account - Closing Account Revival: Closing an account (zero lamports) but data remains readable in same tx
- CPI Re-signer Attack: Exploiting that CPI can sign with PDA seeds the caller controls
- Remaining Accounts Abuse: Exploiting unchecked accounts in
ctx.remaining_accounts - Flashback Attack: Solana transactions can include multiple instructions — attacker can create and exploit account state in same tx
Program-Side Security Checklist
Account Validation (7 checks)
- Every account has an owner check (native) or uses
Account<'info, T>(Anchor) - Every privileged account has a signer check
- PDAs are derived with sufficient unique seeds (no PDA sharing)
- All accounts have discriminator validation (type cosplay prevention)
- Semantically distinct accounts are compared for uniqueness
-
has_one/ manual field comparison for data matching (mint, authority) - Accounts in
remaining_accountsare validated before use
CPI Safety (3 checks)
- CPI target is a known program (
Program<'info, T>or key comparison) -
invoke_signedseeds are deterministic and cannot be replayed - CPI return data is validated, not blindly trusted
Arithmetic (3 checks)
- All math uses
checked_*operations oroverflow-checks = truein Cargo.toml - Token amounts validated against actual balances before transfer
- Fee/reward calculations handle rounding and zero-value edge cases
State Lifecycle (4 checks)
-
initis used instead ofinit_if_needed(prevents front-running) - Closed accounts are zeroed + poisoned (discriminator = 0xFF)
- Upgrade authority is verified or program is immutable in production
- Account realloc operations check size bounds
Client-Side Security Checklist
- Correct cluster (mainnet vs devnet) — program IDs differ
- Transactions are simulated before sending to catch errors early
-
getLatestBlockhashuses confirmed or finalized commitment - Transaction confirmation waits for sufficient commitment level
- Token-2022 extensions (transfer hooks, transfer fees) are handled
- Lookup tables / versioned transactions are used for large account sets
- Priority fees are set appropriately for congested network conditions
Build & IDL Integrity Checklist
Supply-chain and deployment verification — catches mismatches between audited source and deployed program.
- On-chain program matches audited source — verify with
solana program dump+ local rebuild - IDL matches on-chain behavior — generated IDL (Anchor/Codama) reflects actual instruction layouts
- No hand-written Borsh layouts for programs the team owns — use IDL/codegen pipeline (hand-written layouts risk serialization mismatches → type confusion)
- No hand-written IDLs — generate from source via
anchor buildor Shank macros - Codegen outputs checked into git for deterministic builds, or generated in CI with pinned versions
- Anchor version pinned in
Anchor.toml— different Anchor versions produce different discriminators - Solana CLI + Anchor versions aligned across dev, CI, and audit environments
- Program upgrade authority is multisig or governance (not a single EOA) for mainnet deployments
- If upgradeable, upgrade timelock exists to allow users to exit before code changes
Security Review Questions
When reviewing a Solana program, systematically ask:
- Can an attacker pass a fake account (wrong owner / wrong type)?
- Can a non-authorized user trigger this instruction?
- Can CPI be substituted to a malicious program?
- Can
initializebe called again on an existing account? - Are PDAs shared across users or contexts unintentionally?
- Can the same account be passed for two different params?
- Can a closed account be read or revived in a later instruction?
- Do on-chain fields match expected accounts (authority, mint, owner)?
Solana Audit Procedure
Step-by-step execution flow when auditing a Solana program. Adapted from Solana Foundation's recommended operating procedure.
Step 1: Classify the Program
| Question | Why It Matters |
|---|---|
| Anchor or native/Pinocchio? | Determines which constraint system to audit (declarative vs manual) |
| What token programs are used? | SPL Token vs Token-2022 — different extension attack surfaces |
| Is the program upgradeable? | If yes, audit the upgrade authority and governance process |
| Does it use CPI? | Cross-program calls are the #1 privilege escalation vector |
| Are there multiple instructions per tx? | Multi-instruction flows enable flashback attacks |
Step 2: Map Every Account
For each instruction, build the Account Validation Matrix (see above). Every cell must be explicitly checked in code. An empty cell is a potential vulnerability.
Step 3: Trace CPI Chains
For each invoke() / invoke_signed():
- Is the target program ID validated? (
Program<'info, T>or key comparison) - Are signer seeds deterministic and non-replayable?
- Does the CPI return data get validated, or is it blindly trusted?
- Can an attacker substitute a malicious program ID?
Step 4: Verify Arithmetic Safety
- Check
Cargo.tomlforoverflow-checks = true(release profile) - If not present, every arithmetic operation must use
checked_* - Verify token amount calculations handle rounding and zero-value edge cases
- Check for division before multiplication (precision loss)
Step 5: Evaluate Formal Verification (if present)
If the program includes Kani proofs or other formal verification:
- Inventory proofs — count, classify (STRONG/WEAK/VACUOUS), categorize by domain
- Map proofs to security claims — does the proof actually back the claim?
- Identify what is NOT proven — CPI execution, oracle reads, and runtime behavior are never Kani-verifiable
- Check for coupling proofs — does the
verifymodule match the production handler? - Flag vacuous proofs — any proof with contradictory
assume()chains is a finding - Report strength — "N proofs (X% STRONG) covering [categories]"
See Formal Verification for Auditors for proof classification and evaluation methodology.
Step 6: Test with PoCs
Use the testing tools to write targeted PoCs for any findings:
- LiteSVM — Fast in-process execution for exploit PoCs (seconds, not minutes)
- Mollusk — Direct program execution for CU impact analysis
- Surfpool — Integration testing against real mainnet state (Jupiter, Orca, etc.)
See Solana Testing for Auditors for setup guides.
Step 7: Deliverables
For each finding, provide:
- Vulnerability category (from the 9 categories above)
- Affected instruction(s) with account layout
- Exploit scenario — step-by-step attack using concrete accounts
- PoC code (if applicable) using LiteSVM or Mollusk
- Fix recommendation with Anchor and/or native Rust code
- Risk notes for anything touching signing, fees, CPIs, or token transfers
Workflows
| Workflow | Description |
|---|---|
| Anchor Audit | Audit workflow for Anchor framework programs — constraint validation, CPI safety, PDA verification |
| Native Audit | Audit workflow for native Solana programs — manual account deserialization, raw instruction processing |
Resources
| Resource | Description |
|---|---|
| Account Validation | Complete guide to account validation checks: signer, owner, PDA, type, uniqueness |
| Anchor Security | Anchor-specific security patterns: constraints, CPI, init_if_needed, close |
| Solana Patterns | Common vulnerability patterns with code examples and fixes |
| Curated Links | 50+ curated links to official docs, audit reports, security courses, tools, and firms |
| Security Fundamentals | Core Solana security principles, threat model, and best practices |
| Native Security | Native Solana (non-Anchor) security patterns and pitfalls |
| Security Checklists | Audit and client-side checklists for Solana programs |
| Caveats | Solana-specific caveats, gotchas, and edge cases for auditors |
| Formal Verification | Kani proof evaluation: classification, property categories, verify module extract-and-prove pattern |
| Adversarial Test Design | Attack-first test taxonomy, conservation invariant methodology, 10-category checklist |
Notable Solana Security Incidents
| Incident | Date | Root Cause | Loss |
|---|---|---|---|
| Wormhole bridge | Feb 2022 | Missing signer verification on complete_wrapped | $326M |
| Cashio stablecoin | Mar 2022 | Missing crate_collateral_tokens.mint validation | $52M |
| Mango Markets | Oct 2022 | Oracle price manipulation + account borrowing | $116M |
| Crema Finance | Jul 2022 | Fake tick account injection in CPI | $8.8M |
| Slope wallet | Aug 2022 | Private key logging in centralized server | $4.1M |
| Solend | Various | Multiple oracle and liquidation issues | Various |
Ecosystem Context
| Metric | Value |
|---|---|
| Smart contract language | Rust |
| Primary framework | Anchor (>90% of new projects) |
| Block time | ~400ms |
| Transaction model | Multiple instructions per transaction |
| Account size limit | 10MB |
| Compute budget | 200,000 compute units per instruction (adjustable to 1.4M) |
| Token standard | SPL Token / Token-2022 |
| NFT standard | Metaplex Token Metadata |
Integration with Other Skills
| Skill | Connection |
|---|---|
patterns/ | Cross-reference Solana-specific patterns with Solodit database (limited but growing) |
exploit-forensics/ | Wormhole, Cashio, Mango exploits provide forensic case studies |
chain-guides/solana.md | Chain-level context for Solana validator behavior, consensus, fees |
attack-trees/ | Solana-specific attack trees (account confusion, CPI escalation) |
Error Code Reference
Common Solana program error codes encountered during audits. These appear in transaction logs and simulation failures.
Anchor Framework Errors
| Error Code | Error Name | Meaning |
|---|---|---|
0x64 (100) | InstructionMissing | Expected instruction not found in transaction |
0x65 (101) | InstructionFallbackNotFound | No fallback handler for instruction |
0xBB8 (3000) | ConstraintMut | Account not marked as mutable (#[account(mut)] missing) |
0xBB9 (3001) | ConstraintHasOne | has_one constraint failed — account field doesn't match |
0xBBA (3002) | ConstraintSigner | Account is not a signer |
0xBBB (3003) | ConstraintRaw | Custom constraint = <expr> evaluated to false |
0xBBC (3004) | ConstraintOwner | Account owner does not match expected program |
0xBBF (3007) | ConstraintSeeds | PDA seeds do not derive the expected address |
0xBC4 (3012) | ConstraintSpace | Account data space insufficient |
0x7D0 (2000) | DeclaredProgramIdMismatch | declare_id! does not match actual program ID |
0x7D1 (2001) | TryingToInitPayerAsProgramAccount | Payer account reused as program-owned account |
0xBCE (3022) | AccountNotInitialized | Account data is empty / uninitialized |
0xBC0 (3008) | ConstraintExecutable | Account is not an executable program |
0x1770 (6000)+ | Custom errors | Application-specific errors start at 6000 |
SPL Token Program Errors
| Error Code | Error Name | Meaning |
|---|---|---|
0x0 | NotRentExempt | Account balance below rent-exempt minimum |
0x1 | InsufficientFunds | Token balance too low for transfer |
0x3 | InvalidMint | Token account mint does not match expected mint |
0x4 | MintMismatch | Mint of token account does not match instruction mint |
0x5 | OwnerMismatch | Token account owner does not match expected owner |
0xA | AlreadyInUse | Account is already initialized |
0xC | InvalidNumberOfProvidedSigners | Wrong number of multisig signers |
0xD | InvalidNumberOfRequiredSigners | Invalid multisig threshold |
0x11 | AccountFrozen | Token account is frozen — transfers blocked |
System Program / Runtime Errors
| Error Code | Error Name | Meaning |
|---|---|---|
0x0 | InsufficientFundsForRent | Account will drop below rent-exempt after operation |
0x1 | AccountAlreadyInitialized | Cannot reinitialize existing account |
0x3 | AccountNotFound | Referenced account does not exist |
ProgramFailedToComplete | (Runtime) | Program exceeded compute budget or panicked |
PrivilegeEscalation | (Runtime) | CPI attempted to escalate privileges |
AccountDataTooSmall | (Runtime) | Account data buffer too small for deserialization |
Troubleshooting
| Issue | Likely Cause | Solution |
|---|---|---|
| Scanner misses missing-signer vulnerabilities | Native program uses AccountInfo without Anchor constraints | Check all AccountInfo params for explicit is_signer / is_writable validation |
| False positive on PDA seeds | Scanner flags all user-controlled seeds | Verify if seeds are bounded by program logic; canonical bump stored correctly |
| Account confusion not detected | Different account types share same structure size | Check discriminator bytes; Anchor auto-adds 8-byte discriminators |
| CPI privilege escalation missed | invoke_signed seeds not analyzed | Manually trace PDA seed derivation through CPI chain |
| Scanner doesn't flag overflow in release mode | Release builds wrap instead of panic | Flag all non-checked_* math ops; verify overflow-checks = true in Cargo.toml |
| Duplicate account injection not caught | Scanner doesn't check account uniqueness | Verify all instruction accounts are compared for uniqueness where semantically distinct |
Signals
- GitHub stars
- 60
- Forks
- 10
- Last commit
- Feb 2026
Advanced
- Catalog kind
- skill
- Gateway key
solana-scanner- Source
- github.com/0x-shashi/web3-audit-skills