skills/solana-scanner/SKILL.md
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.
npx skillsauth add 0x-shashi/web3-audit-skills skills/solana-scannerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.
| 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) |
| 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 |
| 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 |
| 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 |
Each category shows the risk, an attack example, and prevention for both Anchor and Pinocchio/native Rust.
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);
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);
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);
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");
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)]
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");
Risk: Same account passed as both source and destination → double-counting.
// Both frameworks — explicit key comparison
require!(source.key() != destination.key(), ErrorCode::DuplicateAccounts);
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)?;
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());
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) |
initialize on already-initialized accountctx.remaining_accountsAccount<'info, T> (Anchor)has_one / manual field comparison for data matching (mint, authority)remaining_accounts are validated before useProgram<'info, T> or key comparison)invoke_signed seeds are deterministic and cannot be replayedchecked_* operations or overflow-checks = true in Cargo.tomlinit is used instead of init_if_needed (prevents front-running)getLatestBlockhash uses confirmed or finalized commitmentSupply-chain and deployment verification — catches mismatches between audited source and deployed program.
solana program dump + local rebuildanchor build or Shank macrosAnchor.toml — different Anchor versions produce different discriminatorsWhen reviewing a Solana program, systematically ask:
initialize be called again on an existing account?Step-by-step execution flow when auditing a Solana program. Adapted from Solana Foundation's recommended operating procedure.
| 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 |
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.
For each invoke() / invoke_signed():
Program<'info, T> or key comparison)Cargo.toml for overflow-checks = true (release profile)checked_*If the program includes Kani proofs or other formal verification:
verify module match the production handler?assume() chains is a findingSee Formal Verification for Auditors for proof classification and evaluation methodology.
Use the testing tools to write targeted PoCs for any findings:
See Solana Testing for Auditors for setup guides.
For each finding, provide:
| 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 |
| 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 |
| 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 |
| 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 |
| 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) |
Common Solana program error codes encountered during audits. These appear in transaction logs and simulation failures.
| 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 |
| 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 |
| 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 |
| 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 |
development
Systematically hunt for every variant of a discovered vulnerability across the entire codebase. Use when a bug is found and all instances of the same root cause pattern must be identified, or when performing variant analysis during competitive audits on Code4rena or Sherlock.
testing
Use when the user wants to audit TON smart contracts for security vulnerabilities, scan FunC or Tact contracts for message chain replay, bounce handling, or gas issues, review TON DeFi protocols for actor-model concurrency flaws, or analyze asynchronous message passing security.
tools
Analyze ERC20/ERC721/ERC1155 token implementations for non-standard behavior, fee-on-transfer mechanics, rebasing logic, blacklists, pausability, and integration risks. Use when reviewing protocols that interact with external tokens or implementing token-related features.
testing
Use when the user wants to audit Sui Move smart contracts, scan Sui-specific patterns including object ownership, shared objects, or dynamic fields, review Sui DeFi protocols for object model security issues, or analyze Sui-specific transaction and consensus patterns.