# 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.

- **Kind:** skill
- **Source:** https://github.com/trailofbits/skills
- **Page:** https://forefy.com/skills/14ad9c6d-b553-4de2-bdaa-d32bb564f645
- **API (JSON + files):** https://forefy.com/api/asr/14ad9c6d-b553-4de2-bdaa-d32bb564f645

---

## SKILL.md

---
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.

## agents

```

```

## agents/openai.yaml

```yaml
interface:
  icon_small: "assets/trail-of-bits-mark.svg"
  icon_large: "assets/trail-of-bits-mark.svg"
  brand_color: "#D83A34"
```

## assets

```

```

## assets/trail-of-bits-mark.svg

```

```

## resources

```

```

## resources/VULNERABILITY_PATTERNS.md

### 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

---

