Claude Skill

solana-vulnerability-scanner

Scans Solana programs for 6 critical vulnerabilities including arbitrary CPI, improper PDA validation, missing signer/ownership checks, and sysvar spoofing. Use when auditing Solana/Anchor programs.

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download trailofbits-skills-plugins_building-secure-contracts_skills_solana-vulnerability-scanner-123037e.zip · 11 KB
trailofbits/skills 7234 616 forks CC-BY-SA-4.0 Updated 22h ago
Part of trailofbits/skills — 100 skills

Install

skills CLI npx skills add https://github.com/trailofbits/skills/tree/main/plugins/building-secure-contracts/skills/solana-vulnerability-scanner
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install trailofbits-skills@llmmart
Git git clone https://github.com/trailofbits/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole trailofbits/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Solana Vulnerability Scanner

1. Purpose

Systematically scan Solana programs (native and Anchor framework) for platform-specific security vulnerabilities related to cross-program invocations, account validation, and program-derived addresses. This skill encodes 6 critical vulnerability patterns unique to Solana's account model.

2. When to Use This Skill

  • Auditing Solana programs (native Rust or Anchor)
  • Reviewing cross-program invocation (CPI) logic
  • Validating program-derived address (PDA) implementations
  • Pre-launch security assessment of Solana protocols
  • Reviewing account validation patterns
  • Assessing instruction introspection logic

3. Platform Detection

File Extensions & Indicators

  • Rust files: .rs

Language/Framework Markers

// Native Solana program indicators
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
    program::invoke,
    program::invoke_signed,
};

entrypoint!(process_instruction);

// Anchor framework indicators
use anchor_lang::prelude::*;

#[program]
pub mod my_program {
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // Program logic
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
}

// Common patterns
AccountInfo, Pubkey
invoke(), invoke_signed()
Signer<'info>, Account<'info>
#[account(...)] with constraints
seeds, bump

Project Structure

  • programs/*/src/lib.rs - Program implementation
  • Anchor.toml - Anchor configuration
  • Cargo.toml with solana-program or anchor-lang
  • tests/ - Program tests

Tool Support

  • Trail of Bits Solana Lints: Rust linters for Solana
  • Installation: Add to Cargo.toml
  • anchor test: Built-in testing framework
  • Solana Test Validator: Local testing environment

4. How This Skill Works

When invoked, I will:

  1. Search your codebase for Solana/Anchor programs
  2. Analyze each program for the 6 vulnerability patterns
  3. Report findings with file references and severity, above them a coverage table carrying a verdict for every pattern
  4. Provide fixes for each identified issue
  5. Check account validation and CPI security

5. Example Output


6. Vulnerability Patterns (6 Patterns)

I check for 6 critical vulnerability patterns unique to Solana. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.

Pattern Summary:

  1. Arbitrary CPI ⚠️ CRITICAL - User-controlled program IDs in CPI calls
  2. Improper PDA Validation ⚠️ CRITICAL - Using create_program_address without canonical bump
  3. Missing Ownership Check ⚠️ HIGH - Deserializing accounts without owner validation
  4. Missing Signer Check ⚠️ CRITICAL - Authority operations without is_signer check
  5. Sysvar Account Check ⚠️ HIGH - Spoofed sysvar accounts (pre-Solana 1.8.1)
  6. Improper Instruction Introspection ⚠️ MEDIUM - Absolute indexes allowing reuse

For complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.

7. Scanning Workflow

Step 1: Platform Identification

  1. Verify Solana program (native or Anchor)
  2. Check Solana version (1.8.1+ for sysvar security)
  3. Locate program source (programs/*/src/lib.rs)
  4. Identify framework (native vs Anchor)

Step 2: CPI Security Review

# Find all CPI calls
rg "invoke\(|invoke_signed\(" programs/

# Check for program ID validation before each
# Should see program ID checks immediately before invoke

For each CPI:

  • Program ID validated before invocation
  • Cannot pass user-controlled program accounts
  • Anchor: Uses Program<'info, T> type

Step 3: PDA Validation Check

# Find PDA usage
rg "find_program_address|create_program_address" programs/
rg "seeds.*bump" programs/

# Anchor: Check for seeds constraints
rg "#\[account.*seeds" programs/

For each PDA:

  • Uses find_program_address() or Anchor seeds constraint
  • Bump seed stored and reused
  • Not using user-provided bump

Step 4: Account Validation Sweep

# Find account deserialization
rg "try_from_slice|try_deserialize" programs/

# Should see owner checks before deserialization
rg "\.owner\s*==|\.owner\s*!=" programs/

For each account used:

  • Owner validated before deserialization
  • Signer check for authority accounts
  • Anchor: Uses Account<'info, T> and Signer<'info>

Step 5: Instruction Introspection Review

# Find instruction introspection usage
rg "load_instruction_at|load_current_index|get_instruction_relative" programs/

# Check for checked versions
rg "load_instruction_at_checked|load_current_index_checked" programs/
  • Using checked functions (Solana 1.8.1+)
  • Using relative indexing
  • Proper correlation validation

Step 6: Trail of Bits Solana Lints

# Add to Cargo.toml
[dependencies]
solana-program = "1.17"  # Use latest version

[lints.clippy]
# Enable Solana-specific lints
# (Trail of Bits solana-lints if available)

8. Reporting Format

Coverage Table

Report on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with all 6 rows present:

# Pattern Verdict Evidence
1 Arbitrary CPI n/a this program makes no cross-program invocations
2 Improper PDA Validation
3 Missing Ownership Check
4 Missing Signer Check
5 Sysvar Account Check
6 Improper Instruction Introspection

Each verdict is one of:

  • found — cite file:line and write the finding up in full below.
  • clear — the pattern applies to this program and the program handles it. Name the constraint, account type, or check you searched for, so a reader can repeat the search.
  • n/a — the pattern cannot apply here. Give the reason in one clause ("this program makes no CPI calls"). Not having looked is not n/a. Pattern 5 is version-scoped (pre-Solana 1.8.1): cite the solana-program version the program targets rather than dropping the row, since "targets 1.17, fixed upstream" and "did not look" are otherwise the same answer.

A table with fewer than 6 rows is an incomplete scan and must be reported as one. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done. Six clear verdicts is a result a reader can act on. A report that covers two patterns and says nothing about the other four reads exactly like a clean program, and that is the failure this table exists to prevent.

Finding Template

## [CRITICAL] Arbitrary CPI - Unchecked Program ID

**Location**: `programs/vault/src/lib.rs:145-160` (withdraw function)

**Description**:
The `withdraw` function performs a CPI to transfer SPL tokens without validating that the provided `token_program` account is actually the SPL Token program. An attacker can provide a malicious program that appears to perform a transfer but actually steals tokens or performs unauthorized actions.

**Vulnerable Code**:
```rust
// lib.rs, line 145
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let token_program = &ctx.accounts.token_program;

    // WRONG: No validation of token_program.key()!
    invoke(
        &spl_token::instruction::transfer(...),
        &[
            ctx.accounts.vault.to_account_info(),
            ctx.accounts.destination.to_account_info(),
            ctx.accounts.authority.to_account_info(),
            token_program.to_account_info(),  // UNVALIDATED
        ],
    )?;
    Ok(())
}
```

**Attack Scenario**:
1. Attacker deploys malicious "token program" that logs transfer instruction but doesn't execute it
2. Attacker calls withdraw() providing malicious program as token_program
3. Vault's authority signs the transaction
4. Malicious program receives CPI with vault's signature
5. Malicious program can now impersonate vault and drain real tokens

**Recommendation**:
Use Anchor's `Program<'info, Token>` type:
```rust
use anchor_spl::token::{Token, Transfer};

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut)]
    pub vault: Account<'info, TokenAccount>,
    #[account(mut)]
    pub destination: Account<'info, TokenAccount>,
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,  // Validates program ID automatically
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let cpi_accounts = Transfer {
        from: ctx.accounts.vault.to_account_info(),
        to: ctx.accounts.destination.to_account_info(),
        authority: ctx.accounts.authority.to_account_info(),
    };

    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts,
    );

    anchor_spl::token::transfer(cpi_ctx, amount)?;
    Ok(())
}
```

**References**:
- building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
- Trail of Bits lint: `unchecked-cpi-program-id`

9. Priority Guidelines

Critical (Immediate Fix Required)

  • Arbitrary CPI (attacker-controlled program execution)
  • Improper PDA validation (account spoofing)
  • Missing signer check (unauthorized access)

High (Fix Before Launch)

  • Missing ownership check (fake account data)
  • Sysvar account check (authentication bypass, pre-1.8.1)

Medium (Address in Audit)

  • Improper instruction introspection (logic bypass)

10. Testing Recommendations

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn test_rejects_wrong_program_id() {
        // Provide wrong program ID, should fail
    }

    #[test]
    #[should_panic]
    fn test_rejects_non_canonical_pda() {
        // Provide non-canonical bump, should fail
    }

    #[test]
    #[should_panic]
    fn test_requires_signer() {
        // Call without signature, should fail
    }
}

Integration Tests (Anchor)

import * as anchor from "@coral-xyz/anchor";

describe("security tests", () => {
  it("rejects arbitrary CPI", async () => {
    const fakeTokenProgram = anchor.web3.Keypair.generate();

    try {
      await program.methods
        .withdraw(amount)
        .accounts({
          tokenProgram: fakeTokenProgram.publicKey, // Wrong program
        })
        .rpc();

      assert.fail("Should have rejected fake program");
    } catch (err) {
      // Expected to fail
    }
  });
});

Solana Test Validator

# Run local validator for testing
solana-test-validator

# Deploy and test program
anchor test

11. Additional Resources


12. Quick Reference Checklist

Before completing Solana program audit:

CPI Security (CRITICAL):

  • ALL CPI calls validate program ID before invoke()
  • Cannot use user-provided program accounts
  • Anchor: Uses Program<'info, T> type

PDA Security (CRITICAL):

  • PDAs use find_program_address() or Anchor seeds constraint
  • Bump seed stored and reused (not user-provided)
  • PDA accounts validated against canonical address

Account Validation (HIGH):

  • ALL accounts check owner before deserialization
  • Native: Validates account.owner == expected_program_id
  • Anchor: Uses Account<'info, T> type

Signer Validation (CRITICAL):

  • ALL authority accounts check is_signer
  • Native: Validates account.is_signer == true
  • Anchor: Uses Signer<'info> type

Sysvar Security (HIGH):

  • Using Solana 1.8.1+
  • Using checked functions: load_instruction_at_checked()
  • Sysvar addresses validated

Instruction Introspection (MEDIUM):

  • Using relative indexes for correlation
  • Proper validation between related instructions
  • Cannot reuse same instruction across multiple calls

Testing:

  • Unit tests cover all account validation
  • Integration tests with malicious inputs
  • Local validator testing completed
  • Trail of Bits lints enabled and passing
  • Coverage table emitted with all 6 rows, each carrying a verdict of found, clear or n/a with a reason

13. Rationalizations to Reject

  • "The program is small, so most patterns obviously don't apply." Obvious to whom? An n/a costs one clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from not having checked.
  • "The Trail of Bits lints pass, so the program is clean." The lints cover a subset of these 6 patterns. A clean lint run is one row of evidence, not a verdict on the patterns it never examined. Say which patterns it covered.
  • "I checked the patterns that matter for this program." Deciding which patterns matter is the scan, not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
  • "No findings, so there is nothing to report." A zero-finding scan still emits the full coverage table. That table is the deliverable: it is what distinguishes a program that was examined from one that was glanced at.
  • "Anchor handles account validation." Name the constraint. Anchor validates what the account struct declares and nothing more: #[account(mut)] is not an ownership check, and UncheckedAccount opts out entirely. Cite the attribute, not the framework.
  • "The PDA derivation makes it safe." Derivation is not validation. create_program_address without the canonical bump admits multiple valid addresses, which is pattern 2 in full.
Files (skills)
  • agents
    • openai.yaml 258 B
      interface:
        display_name: "Solana Vulnerability Scanner"
        short_description: "Find security vulnerabilities in Solana and Anchor programs"
        icon_small: "assets/trail-of-bits-mark.svg"
        icon_large: "assets/trail-of-bits-mark.svg"
        brand_color: "#D83A34"
      
  • assets
    • trail-of-bits-mark.svg 3 KB · in bundle
  • resources
    • VULNERABILITY_PATTERNS.md 19.9 KB
      ### 6.1 ARBITRARY CPI (Cross-Program Invocation) ⚠️ CRITICAL
      
      **Description**: Using `invoke()` or `invoke_signed()` with user-controlled program IDs allows attackers to call malicious programs instead of the intended program.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: User-provided program ID without validation
      pub fn transfer_tokens(
          ctx: Context<TransferTokens>,
          amount: u64,
      ) -> Result<()> {
          // User provides token_program account
          let token_program = &ctx.accounts.token_program;
      
          // WRONG: No check that token_program.key() == spl_token::ID!
          invoke(
              &spl_token::instruction::transfer(...),
              &[
                  ctx.accounts.from.to_account_info(),
                  ctx.accounts.to.to_account_info(),
                  token_program.to_account_info(),  // ATTACKER CONTROLLED!
              ],
          )?;
          Ok(())
      }
      
      // VULNERABLE: Native Solana without validation
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let token_program = next_account_info(accounts_iter)?;
      
          // WRONG: No validation of token_program.key
          invoke(
              &transfer_instruction,
              &[from_account, to_account, token_program],  // Unvalidated!
          )?;
          Ok(())
      }
      ```
      
      **What to Check**:
      - [ ] ALL CPI program IDs validated before `invoke()` or `invoke_signed()`
      - [ ] Validation: `program.key() == EXPECTED_PROGRAM_ID`
      - [ ] Cannot pass arbitrary program accounts from user
      - [ ] Anchor: Use `Program<'info, T>` type with constraint
      
      **Mitigation**:
      ```rust
      // SECURE: Validate program ID (Native)
      use spl_token;
      
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let token_program = next_account_info(accounts_iter)?;
      
          // CRITICAL: Validate program ID
          if token_program.key != &spl_token::ID {
              return Err(ProgramError::IncorrectProgramId);
          }
      
          // Safe to invoke
          invoke(
              &spl_token::instruction::transfer(...),
              &[from_account, to_account, token_program],
          )?;
          Ok(())
      }
      
      // SECURE: Use Anchor Program type with constraint
      use anchor_spl::token::{Token, TokenAccount};
      
      #[derive(Accounts)]
      pub struct TransferTokens<'info> {
          #[account(mut)]
          pub from: Account<'info, TokenAccount>,
          #[account(mut)]
          pub to: Account<'info, TokenAccount>,
          pub authority: Signer<'info>,
          // Program<'info, Token> automatically validates program ID
          pub token_program: Program<'info, Token>,
      }
      
      pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
          // Anchor ensures token_program.key() == Token::id()
          let cpi_accounts = Transfer {
              from: ctx.accounts.from.to_account_info(),
              to: ctx.accounts.to.to_account_info(),
              authority: ctx.accounts.authority.to_account_info(),
          };
      
          let cpi_ctx = CpiContext::new(
              ctx.accounts.token_program.to_account_info(),
              cpi_accounts,
          );
      
          anchor_spl::token::transfer(cpi_ctx, amount)?;
          Ok(())
      }
      ```
      
      **Tool Detection**:
      - Trail of Bits lint: `unchecked-cpi-program-id`
      - Look for: `invoke()` without prior program ID check
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
      
      ---
      
      ### 4.2 IMPROPER PDA VALIDATION ⚠️ CRITICAL
      
      **Description**: Program-Derived Addresses (PDAs) can have multiple valid bumps for the same seeds. Using `create_program_address()` without verifying canonical bump allows PDA spoofing attacks.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: Using create_program_address without bump validation
      pub fn withdraw(ctx: Context<Withdraw>, bump: u8) -> Result<()> {
          // User provides bump
          let vault_seeds = &[
              b"vault",
              ctx.accounts.user.key().as_ref(),
              &[bump],  // WRONG: Attacker can provide non-canonical bump!
          ];
      
          let vault = Pubkey::create_program_address(vault_seeds, ctx.program_id)?;
      
          // This vault might not be the canonical PDA!
          // Attacker could create multiple PDAs and drain wrong vault
          Ok(())
      }
      
      // VULNERABLE: Not comparing with find_program_address result
      pub fn initialize(ctx: Context<Initialize>, bump: u8) -> Result<()> {
          let pda_seeds = &[b"state", &[bump]];
          let pda = Pubkey::create_program_address(pda_seeds, ctx.program_id)?;
      
          // WRONG: Not verifying this is the canonical PDA
          // Should check pda == ctx.accounts.pda_account.key()
      }
      ```
      
      **What to Check**:
      - [ ] PDAs use `find_program_address()` to get canonical bump
      - [ ] OR `create_program_address()` result compared with expected PDA
      - [ ] Bump seed stored and reused (not provided by user)
      - [ ] Anchor: Use `seeds` and `bump` constraints
      
      **Mitigation**:
      ```rust
      // SECURE: Use find_program_address (Native)
      pub fn withdraw(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let vault_account = next_account_info(accounts_iter)?;
          let user_account = next_account_info(accounts_iter)?;
      
          // Find canonical PDA with bump
          let (vault_pda, bump) = Pubkey::find_program_address(
              &[b"vault", user_account.key.as_ref()],
              program_id,
          );
      
          // Verify provided account matches canonical PDA
          if vault_account.key != &vault_pda {
              return Err(ProgramError::InvalidAccountData);
          }
      
          // Use bump for signing
          let vault_seeds = &[
              b"vault",
              user_account.key.as_ref(),
              &[bump],
          ];
      
          invoke_signed(
              &transfer_instruction,
              &[vault_account, destination],
              &[vault_seeds],
          )?;
      
          Ok(())
      }
      
      // SECURE: Anchor with seeds constraint
      #[derive(Accounts)]
      pub struct Withdraw<'info> {
          #[account(
              mut,
              seeds = [b"vault", user.key().as_ref()],
              bump,  // Anchor automatically validates canonical bump
          )]
          pub vault: Account<'info, VaultAccount>,
      
          pub user: Signer<'info>,
      }
      
      pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
          // Anchor has already validated vault is canonical PDA
          let bump = *ctx.bumps.get("vault").unwrap();
      
          let vault_seeds = &[
              b"vault",
              ctx.accounts.user.key().as_ref(),
              &[bump],
          ];
      
          // Safe to use in CPI
          let signer_seeds = &[&vault_seeds[..]];
      
          // CPI with PDA signer
          Ok(())
      }
      
      // BETTER: Store bump in account
      #[account]
      pub struct VaultAccount {
          pub bump: u8,  // Store canonical bump
          pub owner: Pubkey,
          pub balance: u64,
      }
      
      #[derive(Accounts)]
      pub struct Initialize<'info> {
          #[account(
              init,
              payer = user,
              space = 8 + 1 + 32 + 8,
              seeds = [b"vault", user.key().as_ref()],
              bump,
          )]
          pub vault: Account<'info, VaultAccount>,
      
          #[account(mut)]
          pub user: Signer<'info>,
          pub system_program: Program<'info, System>,
      }
      
      pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
          let vault = &mut ctx.accounts.vault;
          vault.bump = *ctx.bumps.get("vault").unwrap();  // Store canonical bump
          vault.owner = ctx.accounts.user.key();
          vault.balance = 0;
          Ok(())
      }
      ```
      
      **Tool Detection**:
      - Trail of Bits lint: `improper-pda-validation`
      - Look for: `create_program_address` without `find_program_address` comparison
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/pda_validation
      
      ---
      
      ### 4.3 MISSING OWNERSHIP CHECK ⚠️ HIGH
      
      **Description**: Accounts without owner validation can be spoofed by attackers. User provides account with attacker-controlled data, bypassing program logic.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: Deserializing account without owner check
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let vault_account = next_account_info(accounts_iter)?;
      
          // WRONG: No owner check before deserializing!
          let vault: Vault = Vault::try_from_slice(&vault_account.data.borrow())?;
      
          // vault could be fake account owned by attacker with fake balance!
          if vault.balance >= amount {
              // Process withdrawal using fake balance
          }
      
          Ok(())
      }
      
      // VULNERABLE: Anchor without owner constraint
      #[derive(Accounts)]
      pub struct Withdraw<'info> {
          /// CHECK: This is unsafe - no owner validation!
          pub vault: AccountInfo<'info>,
      }
      ```
      
      **What to Check**:
      - [ ] ALL accounts validated for correct owner before deserialization
      - [ ] Native: Check `account.owner == expected_program_id`
      - [ ] Anchor: Use `Account<'info, T>` type (automatic owner check)
      - [ ] System accounts: Check `account.owner == system_program::ID`
      - [ ] Token accounts: Check `account.owner == spl_token::ID`
      
      **Mitigation**:
      ```rust
      // SECURE: Validate owner before deserializing (Native)
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let vault_account = next_account_info(accounts_iter)?;
      
          // CRITICAL: Validate owner
          if vault_account.owner != program_id {
              return Err(ProgramError::IncorrectProgramId);
          }
      
          // Safe to deserialize - we own this account
          let vault: Vault = Vault::try_from_slice(&vault_account.data.borrow())?;
      
          Ok(())
      }
      
      // SECURE: Use Anchor Account type (automatic validation)
      #[derive(Accounts)]
      pub struct Withdraw<'info> {
          #[account(mut)]
          pub vault: Account<'info, VaultAccount>,  // Anchor checks owner automatically
          pub user: Signer<'info>,
      }
      
      // For third-party program accounts
      #[derive(Accounts)]
      pub struct ProcessToken<'info> {
          #[account(mut)]
          pub token_account: Account<'info, TokenAccount>,  // Validates owner == Token program
          pub token_program: Program<'info, Token>,
      }
      ```
      
      **Tool Detection**:
      - Trail of Bits lint: `missing-ownership-check`
      - Look for: Deserialization without owner validation
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/ownership_check
      
      ---
      
      ### 4.4 MISSING SIGNER CHECK ⚠️ CRITICAL
      
      **Description**: Sensitive operations without `is_signer` validation allow unauthorized users to call functions intended for specific authorities.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: No signer check on authority account
      pub fn withdraw(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          amount: u64,
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let vault = next_account_info(accounts_iter)?;
          let authority = next_account_info(accounts_iter)?;
      
          // WRONG: No check that authority.is_signer == true!
          // Attacker can provide any authority account and withdraw
      
          let vault_data: Vault = Vault::try_from_slice(&vault.data.borrow())?;
      
          // Check authority matches (but attacker provided this!)
          if vault_data.authority != *authority.key {
              return Err(ProgramError::InvalidAccountData);
          }
      
          // Process withdrawal - ATTACKER CAN CALL THIS!
          Ok(())
      }
      
      // VULNERABLE: Anchor without Signer type
      #[derive(Accounts)]
      pub struct Withdraw<'info> {
          #[account(mut)]
          pub vault: Account<'info, VaultAccount>,
          /// CHECK: Missing signer constraint!
          pub authority: AccountInfo<'info>,
      }
      ```
      
      **What to Check**:
      - [ ] ALL authority accounts validated with `is_signer`
      - [ ] Native: Check `account.is_signer == true`
      - [ ] Anchor: Use `Signer<'info>` type (automatic validation)
      - [ ] Access-controlled functions require signer check
      
      **Mitigation**:
      ```rust
      // SECURE: Check is_signer (Native)
      pub fn withdraw(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          amount: u64,
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let vault = next_account_info(accounts_iter)?;
          let authority = next_account_info(accounts_iter)?;
      
          // CRITICAL: Verify authority signed the transaction
          if !authority.is_signer {
              return Err(ProgramError::MissingRequiredSignature);
          }
      
          let vault_data: Vault = Vault::try_from_slice(&vault.data.borrow())?;
      
          // Now safe to check authority matches
          if vault_data.authority != *authority.key {
              return Err(ProgramError::InvalidAccountData);
          }
      
          // Process withdrawal - only if authority signed
          Ok(())
      }
      
      // SECURE: Use Anchor Signer type (automatic validation)
      #[derive(Accounts)]
      pub struct Withdraw<'info> {
          #[account(
              mut,
              has_one = authority,  // Also validate vault.authority == authority.key()
          )]
          pub vault: Account<'info, VaultAccount>,
          pub authority: Signer<'info>,  // Anchor checks is_signer automatically
      }
      
      pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
          // Anchor has already validated:
          // 1. authority.is_signer == true
          // 2. vault.authority == authority.key()
      
          // Safe to proceed with withdrawal
          Ok(())
      }
      
      // For admin functions
      #[derive(Accounts)]
      pub struct UpdateConfig<'info> {
          #[account(
              mut,
              has_one = admin,
          )]
          pub config: Account<'info, Config>,
          pub admin: Signer<'info>,  // Must be signer
      }
      ```
      
      **Tool Detection**:
      - Trail of Bits lint: `missing-signer-check`
      - Look for: Authority checks without `is_signer` validation
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/signer_check
      
      ---
      
      ### 4.5 SYSVAR ACCOUNT CHECK ⚠️ HIGH (Pre-Solana 1.8.1)
      
      **Description**: In Solana versions before 1.8.1, users can pass spoofed sysvar accounts (Instructions, Clock, etc.) to bypass authentication. This affects `load_instruction_at()` and similar functions.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: Using unchecked load functions (Solana < 1.8.1)
      use solana_program::sysvar::instructions;
      
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let instructions_sysvar = next_account_info(accounts_iter)?;
      
          // WRONG: load_instruction_at() doesn't validate sysvar account!
          let current_ix = instructions::load_instruction_at(0, instructions_sysvar)?;
      
          // Attacker can provide fake Instructions sysvar with spoofed instruction data!
          // Bypass authentication by faking previous instruction
      }
      
      // VULNERABLE: load_current_index() without validation
      let current_index = instructions::load_current_index(instructions_sysvar)?;
      ```
      
      **What to Check**:
      - [ ] Using Solana 1.8.1 or higher
      - [ ] Using checked functions: `load_instruction_at_checked()`, `load_current_index_checked()`
      - [ ] NOT using: `load_instruction_at()`, `load_current_index()` (unchecked versions)
      - [ ] Sysvar accounts validated against known addresses
      
      **Mitigation**:
      ```rust
      // OPTION 1: Upgrade to Solana 1.8.1+ and use checked functions
      use solana_program::sysvar::instructions;
      
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let instructions_sysvar = next_account_info(accounts_iter)?;
      
          // SECURE: load_instruction_at_checked validates sysvar account
          let current_ix = instructions::load_instruction_at_checked(
              0,
              instructions_sysvar
          )?;
      
          // Safe - sysvar is validated
          Ok(())
      }
      
      // OPTION 2: Manual validation (if on old Solana version)
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let accounts_iter = &mut accounts.iter();
          let instructions_sysvar = next_account_info(accounts_iter)?;
      
          // Validate sysvar account address
          if instructions_sysvar.key != &solana_program::sysvar::instructions::ID {
              return Err(ProgramError::InvalidAccountData);
          }
      
          // Now safe to use unchecked function
          let current_ix = instructions::load_instruction_at(0, instructions_sysvar)?;
      
          Ok(())
      }
      
      // SECURE: Anchor with address constraint
      #[derive(Accounts)]
      pub struct CheckInstructions<'info> {
          /// CHECK: Validated against known sysvar address
          #[account(address = solana_program::sysvar::instructions::ID)]
          pub instructions_sysvar: AccountInfo<'info>,
      }
      ```
      
      **Tool Detection**:
      - Trail of Bits lint: `unchecked-sysvar-account`
      - Look for: `load_instruction_at()` instead of `load_instruction_at_checked()`
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/sysvar_get
      
      ---
      
      ### 4.6 IMPROPER INSTRUCTION INTROSPECTION ⚠️ MEDIUM
      
      **Description**: Using absolute indexes in instruction introspection allows reusing the same instruction context across multiple program calls. Should use relative indexes to ensure proper correlation.
      
      **Detection Patterns**:
      ```rust
      // VULNERABLE: Absolute index in load_instruction_at
      use solana_program::sysvar::instructions;
      
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let instructions_sysvar = &accounts[0];
      
          // WRONG: Using absolute index 0
          let prev_ix = instructions::load_instruction_at_checked(0, instructions_sysvar)?;
      
          // Attacker can craft transaction where instruction 0 is benign,
          // but instruction 1 (malicious) also loads instruction 0 for validation
          // Same instruction 0 used to validate both instruction 0 and 1!
      }
      
      // VULNERABLE: No correlation between instructions
      pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
          let instructions_sysvar = &ctx.accounts.instructions_sysvar;
      
          // Check that previous instruction was deposit
          let prev_ix = instructions::load_instruction_at_checked(0, instructions_sysvar)?;
      
          // WRONG: Not checking that prev_ix is actually related to current instruction
          // Could be completely unrelated instruction from earlier in transaction
      }
      ```
      
      **What to Check**:
      - [ ] Use relative indexes: `get_instruction_relative(-1, ...)` for previous instruction
      - [ ] Absolute indexes only when specifically intended
      - [ ] Validate correlation between current and referenced instructions
      - [ ] Cannot reuse same instruction validation across multiple calls
      
      **Mitigation**:
      ```rust
      // SECURE: Use relative indexing
      use solana_program::sysvar::instructions;
      
      pub fn process_instruction(
          program_id: &Pubkey,
          accounts: &[AccountInfo],
          instruction_data: &[u8],
      ) -> ProgramResult {
          let instructions_sysvar = &accounts[0];
      
          // Get current instruction index
          let current_index = instructions::load_current_index_checked(instructions_sysvar)?;
      
          // SECURE: Get immediately preceding instruction with relative index
          if current_index > 0 {
              let prev_ix = instructions::load_instruction_at_checked(
                  (current_index - 1) as usize,
                  instructions_sysvar
              )?;
      
              // This is guaranteed to be the instruction immediately before current
              // Validate prev_ix is the expected setup instruction
          }
      
          Ok(())
      }
      
      // BETTER: Use get_instruction_relative (if available)
      let prev_ix = instructions::get_instruction_relative(-1, instructions_sysvar)?;
      // Explicitly relative to current instruction
      
      // SECURE: Additional correlation validation
      pub fn withdraw(ctx: Context<Withdraw>) -> Result<()> {
          let instructions_sysvar = &ctx.accounts.instructions_sysvar;
          let current_ix_index = instructions::load_current_index_checked(instructions_sysvar)?;
      
          // Must have previous instruction
          require!(current_ix_index > 0, ErrorCode::NoPreviousInstruction);
      
          // Get previous instruction
          let prev_ix_index = current_ix_index - 1;
          let prev_ix = instructions::load_instruction_at_checked(
              prev_ix_index as usize,
              instructions_sysvar
          )?;
      
          // Validate previous instruction is deposit to same program
          require!(
              prev_ix.program_id == ctx.program_id,
              ErrorCode::InvalidPreviousProgram
          );
      
          // Validate accounts in previous instruction match expectations
          // This ensures proper correlation between deposit and withdraw
          require!(
              prev_ix.accounts[0].pubkey == ctx.accounts.vault.key(),
              ErrorCode::VaultMismatch
          );
      
          Ok(())
      }
      ```
      
      **References**: building-secure-contracts/not-so-smart-contracts/solana/insecure_instruction_introspection
      
      ---
      
  • SKILL.md 14.1 KB
    ---
    name: solana-vulnerability-scanner
    description: Scans Solana programs for 6 critical vulnerabilities including arbitrary CPI, improper PDA validation, missing signer/ownership checks, and sysvar spoofing. Use when auditing Solana/Anchor programs.
    ---
    
    # Solana Vulnerability Scanner
    
    ## 1. Purpose
    
    Systematically scan Solana programs (native and Anchor framework) for platform-specific security vulnerabilities related to cross-program invocations, account validation, and program-derived addresses. This skill encodes 6 critical vulnerability patterns unique to Solana's account model.
    
    ## 2. When to Use This Skill
    
    - Auditing Solana programs (native Rust or Anchor)
    - Reviewing cross-program invocation (CPI) logic
    - Validating program-derived address (PDA) implementations
    - Pre-launch security assessment of Solana protocols
    - Reviewing account validation patterns
    - Assessing instruction introspection logic
    
    ## 3. Platform Detection
    
    ### File Extensions & Indicators
    - **Rust files**: `.rs`
    
    ### Language/Framework Markers
    ```rust
    // Native Solana program indicators
    use solana_program::{
        account_info::AccountInfo,
        entrypoint,
        entrypoint::ProgramResult,
        pubkey::Pubkey,
        program::invoke,
        program::invoke_signed,
    };
    
    entrypoint!(process_instruction);
    
    // Anchor framework indicators
    use anchor_lang::prelude::*;
    
    #[program]
    pub mod my_program {
        pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
            // Program logic
        }
    }
    
    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(mut)]
        pub authority: Signer<'info>,
    }
    
    // Common patterns
    AccountInfo, Pubkey
    invoke(), invoke_signed()
    Signer<'info>, Account<'info>
    #[account(...)] with constraints
    seeds, bump
    ```
    
    ### Project Structure
    - `programs/*/src/lib.rs` - Program implementation
    - `Anchor.toml` - Anchor configuration
    - `Cargo.toml` with `solana-program` or `anchor-lang`
    - `tests/` - Program tests
    
    ### Tool Support
    - **Trail of Bits Solana Lints**: Rust linters for Solana
    - Installation: Add to Cargo.toml
    - **anchor test**: Built-in testing framework
    - **Solana Test Validator**: Local testing environment
    
    ---
    
    ## 4. How This Skill Works
    
    When invoked, I will:
    
    1. **Search your codebase** for Solana/Anchor programs
    2. **Analyze each program** for the 6 vulnerability patterns
    3. **Report findings** with file references and severity, above them a coverage table carrying a verdict for every pattern
    4. **Provide fixes** for each identified issue
    5. **Check account validation** and CPI security
    
    ---
    
    ## 5. Example Output
    
    ---
    
    ## 6. Vulnerability Patterns (6 Patterns)
    
    I check for 6 critical vulnerability patterns unique to Solana. For detailed detection patterns, code examples, mitigations, and testing strategies, see [VULNERABILITY_PATTERNS.md](resources/VULNERABILITY_PATTERNS.md).
    
    ### Pattern Summary:
    
    1. **Arbitrary CPI** ⚠️ CRITICAL - User-controlled program IDs in CPI calls
    2. **Improper PDA Validation** ⚠️ CRITICAL - Using create_program_address without canonical bump
    3. **Missing Ownership Check** ⚠️ HIGH - Deserializing accounts without owner validation
    4. **Missing Signer Check** ⚠️ CRITICAL - Authority operations without is_signer check
    5. **Sysvar Account Check** ⚠️ HIGH - Spoofed sysvar accounts (pre-Solana 1.8.1)
    6. **Improper Instruction Introspection** ⚠️ MEDIUM - Absolute indexes allowing reuse
    
    For complete vulnerability patterns with code examples, see [VULNERABILITY_PATTERNS.md](resources/VULNERABILITY_PATTERNS.md).
    
    ## 7. Scanning Workflow
    
    ### Step 1: Platform Identification
    1. Verify Solana program (native or Anchor)
    2. Check Solana version (1.8.1+ for sysvar security)
    3. Locate program source (`programs/*/src/lib.rs`)
    4. Identify framework (native vs Anchor)
    
    ### Step 2: CPI Security Review
    ```bash
    # Find all CPI calls
    rg "invoke\(|invoke_signed\(" programs/
    
    # Check for program ID validation before each
    # Should see program ID checks immediately before invoke
    ```
    
    For each CPI:
    - [ ] Program ID validated before invocation
    - [ ] Cannot pass user-controlled program accounts
    - [ ] Anchor: Uses `Program<'info, T>` type
    
    ### Step 3: PDA Validation Check
    ```bash
    # Find PDA usage
    rg "find_program_address|create_program_address" programs/
    rg "seeds.*bump" programs/
    
    # Anchor: Check for seeds constraints
    rg "#\[account.*seeds" programs/
    ```
    
    For each PDA:
    - [ ] Uses `find_program_address()` or Anchor `seeds` constraint
    - [ ] Bump seed stored and reused
    - [ ] Not using user-provided bump
    
    ### Step 4: Account Validation Sweep
    ```bash
    # Find account deserialization
    rg "try_from_slice|try_deserialize" programs/
    
    # Should see owner checks before deserialization
    rg "\.owner\s*==|\.owner\s*!=" programs/
    ```
    
    For each account used:
    - [ ] Owner validated before deserialization
    - [ ] Signer check for authority accounts
    - [ ] Anchor: Uses `Account<'info, T>` and `Signer<'info>`
    
    ### Step 5: Instruction Introspection Review
    ```bash
    # Find instruction introspection usage
    rg "load_instruction_at|load_current_index|get_instruction_relative" programs/
    
    # Check for checked versions
    rg "load_instruction_at_checked|load_current_index_checked" programs/
    ```
    
    - [ ] Using checked functions (Solana 1.8.1+)
    - [ ] Using relative indexing
    - [ ] Proper correlation validation
    
    ### Step 6: Trail of Bits Solana Lints
    ```toml
    # Add to Cargo.toml
    [dependencies]
    solana-program = "1.17"  # Use latest version
    
    [lints.clippy]
    # Enable Solana-specific lints
    # (Trail of Bits solana-lints if available)
    ```
    
    ---
    
    ## 8. Reporting Format
    
    ### Coverage Table
    
    Report on every pattern in §6, whether or not it turned anything up. Emit this table above the findings, with
    all 6 rows present:
    
    | # | Pattern | Verdict | Evidence |
    |---|---------|---------|----------|
    | 1 | Arbitrary CPI | `n/a` | this program makes no cross-program invocations |
    | 2 | Improper PDA Validation | | |
    | 3 | Missing Ownership Check | | |
    | 4 | Missing Signer Check | | |
    | 5 | Sysvar Account Check | | |
    | 6 | Improper Instruction Introspection | | |
    
    Each verdict is one of:
    
    - **`found`** — cite `file:line` and write the finding up in full below.
    - **`clear`** — the pattern applies to this program and the program handles it. Name the constraint, account
      type, or check you searched for, so a reader can repeat the search.
    - **`n/a`** — the pattern cannot apply here. Give the reason in one clause ("this program makes no CPI calls").
      Not having looked is not `n/a`. Pattern 5 is version-scoped (pre-Solana 1.8.1): cite the `solana-program`
      version the program targets rather than dropping the row, since "targets 1.17, fixed upstream" and "did not
      look" are otherwise the same answer.
    
    A table with fewer than 6 rows is an incomplete scan and must be reported as one. A row whose Verdict cell is empty is incomplete in the same way: row 1 above is filled in to show the shape, and every row is filled in the same way before the report is done. Six `clear` verdicts is a
    result a reader can act on. A report that covers two patterns and says nothing about the other four reads
    exactly like a clean program, and that is the failure this table exists to prevent.
    
    ### Finding Template
    ````markdown
    ## [CRITICAL] Arbitrary CPI - Unchecked Program ID
    
    **Location**: `programs/vault/src/lib.rs:145-160` (withdraw function)
    
    **Description**:
    The `withdraw` function performs a CPI to transfer SPL tokens without validating that the provided `token_program` account is actually the SPL Token program. An attacker can provide a malicious program that appears to perform a transfer but actually steals tokens or performs unauthorized actions.
    
    **Vulnerable Code**:
    ```rust
    // lib.rs, line 145
    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let token_program = &ctx.accounts.token_program;
    
        // WRONG: No validation of token_program.key()!
        invoke(
            &spl_token::instruction::transfer(...),
            &[
                ctx.accounts.vault.to_account_info(),
                ctx.accounts.destination.to_account_info(),
                ctx.accounts.authority.to_account_info(),
                token_program.to_account_info(),  // UNVALIDATED
            ],
        )?;
        Ok(())
    }
    ```
    
    **Attack Scenario**:
    1. Attacker deploys malicious "token program" that logs transfer instruction but doesn't execute it
    2. Attacker calls withdraw() providing malicious program as token_program
    3. Vault's authority signs the transaction
    4. Malicious program receives CPI with vault's signature
    5. Malicious program can now impersonate vault and drain real tokens
    
    **Recommendation**:
    Use Anchor's `Program<'info, Token>` type:
    ```rust
    use anchor_spl::token::{Token, Transfer};
    
    #[derive(Accounts)]
    pub struct Withdraw<'info> {
        #[account(mut)]
        pub vault: Account<'info, TokenAccount>,
        #[account(mut)]
        pub destination: Account<'info, TokenAccount>,
        pub authority: Signer<'info>,
        pub token_program: Program<'info, Token>,  // Validates program ID automatically
    }
    
    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let cpi_accounts = Transfer {
            from: ctx.accounts.vault.to_account_info(),
            to: ctx.accounts.destination.to_account_info(),
            authority: ctx.accounts.authority.to_account_info(),
        };
    
        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );
    
        anchor_spl::token::transfer(cpi_ctx, amount)?;
        Ok(())
    }
    ```
    
    **References**:
    - building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
    - Trail of Bits lint: `unchecked-cpi-program-id`
    ````
    
    ---
    
    ## 9. Priority Guidelines
    
    ### Critical (Immediate Fix Required)
    - Arbitrary CPI (attacker-controlled program execution)
    - Improper PDA validation (account spoofing)
    - Missing signer check (unauthorized access)
    
    ### High (Fix Before Launch)
    - Missing ownership check (fake account data)
    - Sysvar account check (authentication bypass, pre-1.8.1)
    
    ### Medium (Address in Audit)
    - Improper instruction introspection (logic bypass)
    
    ---
    
    ## 10. Testing Recommendations
    
    ### Unit Tests
    ```rust
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        #[should_panic]
        fn test_rejects_wrong_program_id() {
            // Provide wrong program ID, should fail
        }
    
        #[test]
        #[should_panic]
        fn test_rejects_non_canonical_pda() {
            // Provide non-canonical bump, should fail
        }
    
        #[test]
        #[should_panic]
        fn test_requires_signer() {
            // Call without signature, should fail
        }
    }
    ```
    
    ### Integration Tests (Anchor)
    ```typescript
    import * as anchor from "@coral-xyz/anchor";
    
    describe("security tests", () => {
      it("rejects arbitrary CPI", async () => {
        const fakeTokenProgram = anchor.web3.Keypair.generate();
    
        try {
          await program.methods
            .withdraw(amount)
            .accounts({
              tokenProgram: fakeTokenProgram.publicKey, // Wrong program
            })
            .rpc();
    
          assert.fail("Should have rejected fake program");
        } catch (err) {
          // Expected to fail
        }
      });
    });
    ```
    
    ### Solana Test Validator
    ```bash
    # Run local validator for testing
    solana-test-validator
    
    # Deploy and test program
    anchor test
    ```
    
    ---
    
    ## 11. Additional Resources
    
    - **Building Secure Contracts**: `building-secure-contracts/not-so-smart-contracts/solana/`
    - **Trail of Bits Solana Lints**: https://github.com/trailofbits/solana-lints
    - **Anchor Documentation**: https://www.anchor-lang.com/
    - **Solana Program Library**: https://github.com/solana-labs/solana-program-library
    - **Solana Cookbook**: https://solanacookbook.com/
    
    ---
    
    ## 12. Quick Reference Checklist
    
    Before completing Solana program audit:
    
    **CPI Security (CRITICAL)**:
    - [ ] ALL CPI calls validate program ID before `invoke()`
    - [ ] Cannot use user-provided program accounts
    - [ ] Anchor: Uses `Program<'info, T>` type
    
    **PDA Security (CRITICAL)**:
    - [ ] PDAs use `find_program_address()` or Anchor `seeds` constraint
    - [ ] Bump seed stored and reused (not user-provided)
    - [ ] PDA accounts validated against canonical address
    
    **Account Validation (HIGH)**:
    - [ ] ALL accounts check owner before deserialization
    - [ ] Native: Validates `account.owner == expected_program_id`
    - [ ] Anchor: Uses `Account<'info, T>` type
    
    **Signer Validation (CRITICAL)**:
    - [ ] ALL authority accounts check `is_signer`
    - [ ] Native: Validates `account.is_signer == true`
    - [ ] Anchor: Uses `Signer<'info>` type
    
    **Sysvar Security (HIGH)**:
    - [ ] Using Solana 1.8.1+
    - [ ] Using checked functions: `load_instruction_at_checked()`
    - [ ] Sysvar addresses validated
    
    **Instruction Introspection (MEDIUM)**:
    - [ ] Using relative indexes for correlation
    - [ ] Proper validation between related instructions
    - [ ] Cannot reuse same instruction across multiple calls
    
    **Testing**:
    - [ ] Unit tests cover all account validation
    - [ ] Integration tests with malicious inputs
    - [ ] Local validator testing completed
    - [ ] Trail of Bits lints enabled and passing
    - [ ] Coverage table emitted with all 6 rows, each carrying a verdict of `found`, `clear` or `n/a` with a reason
    
    ---
    
    ## 13. Rationalizations to Reject
    
    - **"The program is small, so most patterns obviously don't apply."** Obvious to whom? An `n/a` costs one
      clause and makes the judgment reviewable. Silence records nothing, and a reader cannot tell it apart from
      not having checked.
    - **"The Trail of Bits lints pass, so the program is clean."** The lints cover a subset of these 6 patterns.
      A clean lint run is one row of evidence, not a verdict on the patterns it never examined. Say which
      patterns it covered.
    - **"I checked the patterns that matter for this program."** Deciding which patterns matter *is* the scan,
      not a precondition for starting it. Rank by severity after the table is complete, not by leaving rows out.
    - **"No findings, so there is nothing to report."** A zero-finding scan still emits the full coverage table.
      That table is the deliverable: it is what distinguishes a program that was examined from one that was
      glanced at.
    - **"Anchor handles account validation."** Name the constraint. Anchor validates what the account struct
      declares and nothing more: `#[account(mut)]` is not an ownership check, and `UncheckedAccount` opts out
      entirely. Cite the attribute, not the framework.
    - **"The PDA derivation makes it safe."** Derivation is not validation. `create_program_address` without the
      canonical bump admits multiple valid addresses, which is pattern 2 in full.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related