Agent skill
solana-scanner
Use 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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/solana-scanner
SKILL.md
Solana Scanner Skill
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 |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?