# solana-auditor

Security audit of Solana/Rust programs while you develop. Trigger on "audit", "check this program", "review for security". Modes - default (full repo) or a specific filename.

- **Kind:** skill
- **Source:** https://github.com/sanbir/solana-auditor-skills
- **Page:** https://forefy.com/skills/7d1d615b-4d41-481c-8c06-a18a25757cf4
- **API (JSON + files):** https://forefy.com/api/asr/7d1d615b-4d41-481c-8c06-a18a25757cf4

---

## README.md

# Solana Auditor

A security agent for **Solana programs**.

Attribution: this fork keeps the v2 packaging and audit workflow lineage from [pashov/skills](https://github.com/pashov/skills), adapted for Solana programs.

Built for:

- **Program developers** who want a security pass before shipping instruction changes
- **Security researchers** who need rapid coverage over handlers, PDAs, and CPIs
- **Auditors** who want a structured first pass over account validation and state transitions

It is not a substitute for a full audit. It is the fast pass you should run before you trust a program.

## Demo

_Portrayed below: running the skill in a terminal workflow_

![Running solana-auditor in terminal](../static/skill_pag.gif)

## Usage

```bash
/solana-auditor
/solana-auditor --deep
/solana-auditor programs/vault/src/lib.rs
/solana-auditor --file-output
```

## Architecture (v3)

8 specialized parallel agents (sonnet) + optional protocol agent (opus for --deep):

| Agent | Focus |
|-------|-------|
| 1. Vector Scan | All attack vectors from vector bundle |
| 2. Math Precision | Arithmetic, rounding, `as` truncation, decimals |
| 3. Access Control | Signer, owner, PDA authority, initialization |
| 4. Economic Security | Oracles, token quirks, CPI trust, value extraction |
| 5. Execution Trace | Post-CPI staleness, serialization, remaining_accounts |
| 6. Invariant | Conservation laws, state couplings, round-trips |
| 7. Periphery | Utility modules, helpers, serialization code |
| 8. First Principles | Assumption extraction and violation |
| 9. Protocol (--deep) | DeFi-specific checklists (lending, AMM, vault, staking, bridge, governance) |

## Coverage

- **105+ attack vectors** tuned for Solana program security
- **8 specialized hacking agents** for deep parallel analysis
- **4-gate validation** with confidence scoring and lead tracking
- **--deep mode** adds protocol-specific DeFi analysis

## What It Looks For

- missing signer / writable / owner checks and type cosplay
- PDA seed collisions, bump misuse, and close/reinit bugs
- CPI trust errors, stale-account assumptions after CPI, return value ignoring
- Token and Token-2022 quirks, transfer-hook exposure, and authority mixups
- `as` truncation, saturating math abuse, f64 in financial logic
- initialization frontruns and unsafe authority rotation
- oracle / fee / slippage / liquidation logic bugs
- remaining_accounts injection and instruction introspection bypass
- broken invariants, state coupling gaps, round-trip exploits
- compute- or loop-driven denial of service

## Tips

- **Target instruction handlers and account-validation code first.** Those files usually contain the real trust boundaries.
- **Use `--deep` for CPI-heavy, PDA-heavy, liquidation-sensitive, or oracle-dependent programs.** The extra pass pays off when state changes span several accounts and handlers.

## SKILL.md

---
name: solana-auditor
description: Security audit of Solana/Rust programs while you develop. Trigger on "audit", "check this program", "review for security". Modes - default (full repo) or a specific filename.
---

# Solana Program Security Audit

You are the orchestrator of a parallelized Solana smart contract security audit.

## Mode Selection

**Exclude pattern:** skip directories `tests/`, `test/`, `migrations/`, `scripts/`, `target/`, `node_modules/` and files matching `*_test.rs`, `*_tests.rs`, `test_*.rs`, `tests.rs`, `mod.rs` (unless it contains instruction handlers).

- **Default** (no arguments): scan all `.rs` files in the program directory using the exclude pattern. Use Bash `find` (not Glob).
- **`$filename ...`**: scan the specified file(s) only.

**Flags:**

- `--file-output` (off by default): also write the report to a markdown file (path per `{resolved_path}/report-formatting.md`). Never write a report file unless explicitly passed.
- `--deep`: also spawn the Solana protocol analysis agent (Agent 9, opus). Use for thorough reviews of DeFi protocols. Slower and more costly.

## Orchestration

**Turn 1 — Discover.** Print the banner, then make these parallel tool calls in one message:

a. Bash `find` for in-scope `.rs` files per mode selection
b. Glob for `**/references/attack-vectors/attack-vectors-1.md` — extract the `references/` directory (two levels up) as `{resolved_path}`
c. ToolSearch `select:Agent`
d. Read the local `VERSION` file from the same directory as this skill
e. Bash `curl -sf https://raw.githubusercontent.com/sanbir/solana-auditor-skills/main/solana-auditor/VERSION`
f. Bash `mktemp -d /tmp/audit-XXXXXX` → store as `{bundle_dir}`

If the remote VERSION fetch succeeds and differs from local, print `⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/sanbir/solana-auditor-skills`. If it fails, skip silently.

**Turn 2 — Prepare.** In one message, make parallel tool calls: (a) Read `{resolved_path}/report-formatting.md`, (b) Read `{resolved_path}/judging.md`.

Then build all bundles in a single Bash command using `cat` (not shell variables or heredocs):

1. `{bundle_dir}/source.md` — ALL in-scope `.rs` files, each with a `### path` header and fenced code block.
2. Agent bundles = `source.md` + agent-specific files:

| Bundle               | Appended files (relative to `{resolved_path}`)                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `agent-1-bundle.md`  | `attack-vectors/attack-vectors-1.md` + `attack-vectors/attack-vectors-2.md` + `attack-vectors/attack-vectors-3.md` + `attack-vectors/attack-vectors-4.md` + `attack-vectors/attack-vectors-5.md` + `hacking-agents/vector-scan-agent.md` + `hacking-agents/shared-rules.md` |
| `agent-2-bundle.md`  | `hacking-agents/math-precision-agent.md` + `hacking-agents/shared-rules.md`                                     |
| `agent-3-bundle.md`  | `hacking-agents/access-control-agent.md` + `hacking-agents/shared-rules.md`                                     |
| `agent-4-bundle.md`  | `hacking-agents/economic-security-agent.md` + `hacking-agents/shared-rules.md`                                  |
| `agent-5-bundle.md`  | `hacking-agents/execution-trace-agent.md` + `hacking-agents/shared-rules.md`                                    |
| `agent-6-bundle.md`  | `hacking-agents/invariant-agent.md` + `hacking-agents/shared-rules.md`                                          |
| `agent-7-bundle.md`  | `hacking-agents/periphery-agent.md` + `hacking-agents/shared-rules.md`                                          |
| `agent-8-bundle.md`  | `hacking-agents/first-principles-agent.md` + `hacking-agents/shared-rules.md`                                   |

Print line counts for every bundle and `source.md`. Do NOT inline file content into agent prompts.

**Turn 3 — Spawn.** In one message, spawn all 8 agents as parallel foreground Agent calls. Prompt template (substitute real values):

```
Your bundle file is {bundle_dir}/agent-N-bundle.md (XXXX lines).
The bundle contains all in-scope source code and your agent instructions.
Read the bundle fully before producing findings.
```

If `--deep` is set, also spawn Agent 9 (Solana protocol analysis) with `model: "opus"`. Agent 9 receives the in-scope `.rs` file paths and the instruction: your reference directory is `{resolved_path}`. Read `{resolved_path}/hacking-agents/solana-protocol-agent.md` for your full instructions.

**Turn 4 — Deduplicate, validate & output.** Single-pass: deduplicate all agent results, gate-evaluate, and produce the final report in one turn. Do NOT print an intermediate dedup list — go straight to the report.

1. **Deduplicate.** Parse every FINDING and LEAD from all agents. Group by `group_key` field (format: `Program | handler | bug-class`). Exact-match first; then merge synonymous bug_class tags sharing the same program and handler. Keep the best version per group, number sequentially, annotate `[agents: N]`.

   Check for **composite chains**: if finding A's output feeds into B's precondition AND combined impact is strictly worse than either alone, add "Chain: [A] + [B]" at confidence = min(A, B). Most audits have 0-2.

2. **Gate evaluation.** Run each deduplicated finding through the four gates in `judging.md` (do not skip or reorder). Evaluate each finding exactly once — do not revisit after verdict.

   **Single-pass protocol:** evaluate every relevant code path ONCE in fixed order (initialize → deposit/stake → process/swap → withdraw/unstake → claim → close). One-line verdict per path: `BLOCKS`, `ALLOWS`, `IRRELEVANT`, or `UNCERTAIN`. Commit after all paths — do not re-examine. `UNCERTAIN` = `ALLOWS`.

3. **Lead promotion & rejection guardrails.**
   - Promote LEAD → FINDING (confidence 75) if: complete exploit chain traced in source, OR `[agents: 2+]` demoted (not rejected) the same issue.
   - `[agents: 2+]` does NOT override a concrete refutation — demote to LEAD if refutation is uncertain.
   - No deployer-intent reasoning — evaluate what the code _allows_, not how the deployer _might_ use it.

4. **Fix verification** (confidence >= 80 only): trace the attack with fix applied; verify no new DoS, CPI failures, or broken invariants; list all locations if the pattern repeats. If no safe fix exists, omit it with a note.

5. **Format and print** per `report-formatting.md`. Exclude rejected items. If `--file-output`: also write to file.

## Banner

Before doing anything else, print this exactly:

```

███████╗ ██████╗ ██╗      █████╗ ███╗   ██╗ █████╗      █████╗ ██╗   ██╗██████╗ ██╗████████╗ ██████╗ ██████╗
██╔════╝██╔═══██╗██║     ██╔══██╗████╗  ██║██╔══██╗    ██╔══██╗██║   ██║██╔══██╗██║╚══██╔══╝██╔═══██╗██╔══██╗
███████╗██║   ██║██║     ███████║██╔██╗ ██║███████║    ███████║██║   ██║██║  ██║██║   ██║   ██║   ██║██████╔╝
╚════██║██║   ██║██║     ██╔══██║██║╚██╗██║██╔══██║    ██╔══██║██║   ██║██║  ██║██║   ██║   ██║   ██║██╔══██╗
███████║╚██████╔╝███████╗██║  ██║██║ ╚████║██║  ██║    ██║  ██║╚██████╔╝██████╔╝██║   ██║   ╚██████╔╝██║  ██║
╚══════╝ ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝    ╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚═╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝

```

## VERSION

```

```

## assets

```

```

## assets/docs

```

```

## assets/docs/README.md

# Project Docs

**⚠️ Work in Progress — this feature is not ready yet.**

Drop any context that helps the auditor understand what the protocol is supposed to do:

- Design docs and specs
- Intended invariants
- Plain-English descriptions of protocol behavior
- Known limitations or accepted tradeoffs

Files can be plain text or markdown. To reference online docs, create a file containing one URL per line — they will be fetched and read automatically.

## assets/findings

```

```

## assets/findings/README.md

# Findings

**⚠️ Work in Progress — this feature is not ready yet.**

This directory holds two kinds of reports:

- **Reports from previous `/solidity-auditor` runs** — written automatically as `{project-name}-pashov-ai-audit-report-{timestamp}.md` each time the skill runs.
- **External audit reports** — drop any third-party or manual audit `.md` files here.

On each run the skill reads every file in this directory and re-verifies whether previously reported issues still exist in the current code. Issues still present are carried forward with a "Previously reported — still present" note. Issues that are no longer present are silently skipped.

## references

```

```

## references/attack-vectors

```

```

## references/attack-vectors/attack-vectors-1.md

# Attack Vectors Reference — Account Validation & Authorization (1/4)

> Part 1 of 5 · Vectors 1–25 of 105 total
> Covers: signer checks, ownership, discriminators, account constraints, reinitialization, account validation chains

---

## V1 — Missing Signer Check

**Detect:** `AccountInfo<'info>` or `UncheckedAccount<'info>` for authority/admin accounts instead of `Signer<'info>`. In native: `next_account_info` on authority without `if !account.is_signer` check.

**Vulnerable:**
```rust
pub struct Withdraw<'info> {
    #[account(mut)]
    pub vault: Account<'info, Vault>,
    pub authority: AccountInfo<'info>,  // NOT Signer — anyone can pass any pubkey
}
```

**Exploit:** Attacker passes the vault owner's pubkey as `authority` without signing. Drains vault.

**Secure:**
```rust
pub authority: Signer<'info>,  // Anchor enforces is_signer
// Native:
if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); }
```

---

## V2 — Missing Owner Check on Deserialized Account

**Detect:** `try_from_slice()` / `unpack()` / `BorshDeserialize` on account data without prior `account.owner == expected_program_id` check. In Anchor: `AccountInfo<'info>` with `/// CHECK:` used instead of `Account<'info, T>`.

**Vulnerable:**
```rust
let vault: Vault = Vault::try_from_slice(&vault_account.data.borrow())?;
// No check: vault_account.owner == program_id
// Attacker creates fake account with identical layout, owned by their program
```

**Exploit:** Attacker crafts account with spoofed balance/authority fields. Program trusts the data.

**Secure:**
```rust
if vault_account.owner != program_id { return Err(ProgramError::IncorrectProgramId); }
// Anchor: Account<'info, Vault> auto-checks owner
```

---

## V3 — Type Cosplay — Missing Discriminator

**Detect:** In native programs: `BorshDeserialize` structs without an 8-byte discriminant field at offset 0. `try_from_slice()` without discriminator check. In Anchor: `AccountLoader<'info, T>` (zero-copy accounts) — does NOT auto-check discriminators unlike `Account<'info, T>`.

**Vulnerable:**
```rust
#[derive(BorshDeserialize)]
pub struct User { authority: Pubkey, balance: u64 }
// No discriminant field — AdminConfig has same layout, can be substituted

// Anchor zero-copy: AccountLoader also skips discriminator check!
#[account(mut)]
pub user: AccountLoader<'info, User>,  // type cosplay possible
```

**Exploit:** Attacker passes `AdminConfig` account where `User` is expected. Data layouts overlap — attacker's pubkey becomes the "authority." With `AccountLoader`, zero-copy deserialization bypasses the discriminator check that `Account<'info, T>` performs automatically.

**Secure:**
```rust
pub struct User { discriminant: [u8; 8], authority: Pubkey, balance: u64 }
// Anchor: #[account] macro auto-generates discriminator
// For AccountLoader: use Account<'info, T> when possible, or add manual discriminator check
```

---

## V4 — Reinitialization Attack

**Detect:** `init` instructions without `is_initialized` flag check (native). `init_if_needed` without post-init state validation (Anchor). Missing discriminator check before writing initialization data.

**Vulnerable:**
```rust
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.config.authority = ctx.accounts.signer.key();  // overwrites existing authority
    Ok(())
}
// No check whether config was already initialized
```

**Exploit:** Attacker calls `initialize` again to overwrite authority with their own key.

**Secure:**
```rust
// Native: if config.is_initialized { return Err(AlreadyInitialized); }
// Anchor: #[account(init, ...)] prevents reinit via discriminator check
```

---

## V5 — init_if_needed Without State Validation

**Detect:** `init_if_needed` in Anchor `#[account(...)]` constraints without subsequent validation of existing state fields when account already exists.

**Vulnerable:**
```rust
#[account(init_if_needed, payer = user, space = 8 + UserState::INIT_SPACE,
          seeds = [b"user", pool.key().as_ref()], bump)]
pub user_state: Account<'info, UserState>,
// If account exists, no check that user_state.owner == user.key()
```

**Exploit:** Attacker pre-creates the PDA with their own authority before the legitimate user. When legitimate user calls, account already exists with attacker's authority.

**Secure:**
```rust
// Either use `init` (fails if exists) or validate existing state:
if user_state.is_initialized {
    require!(user_state.owner == user.key(), Unauthorized);
}
```

---

## V6 — Missing has_one / Data Mismatch

**Detect:** `Account<'info, T>` with `#[account(mut)]` but no `has_one`, `constraint`, or manual field comparison linking it to other accounts in the instruction.

**Vulnerable:**
```rust
#[account(mut)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
// No check: vault.authority == authority.key()
// Attacker passes their own vault
```

**Exploit:** Attacker passes a vault they control instead of the victim's vault.

**Secure:**
```rust
#[account(mut, has_one = authority)]
pub vault: Account<'info, Vault>,
```

---

## V7 — Missing Writable Annotation

**Detect:** Account modified in instruction handler but missing `#[account(mut)]` in Anchor. State changes are silently discarded at end of instruction.

**Vulnerable:**
```rust
#[account]  // missing `mut`
pub user_state: Account<'info, UserState>,
// In handler: user_state.balance += amount;  // change silently discarded
```

**Exploit:** State updates never persist. Can cause accounting desync — user deposited but balance unchanged.

**Secure:**
```rust
#[account(mut)]
pub user_state: Account<'info, UserState>,
```

---

## V8 — UncheckedAccount Without Manual Validation

**Detect:** `UncheckedAccount<'info>` or `AccountInfo<'info>` with `/// CHECK:` comment but no actual validation code (owner check, key comparison, PDA derivation).

**Vulnerable:**
```rust
/// CHECK: trust me bro
pub oracle: UncheckedAccount<'info>,
// No owner check, no key comparison — attacker can pass any account
```

**Exploit:** Attacker passes fake oracle account with manipulated price data.

**Secure:**
```rust
/// CHECK: Validated below
pub oracle: UncheckedAccount<'info>,
// In handler: require!(*oracle.owner == PYTH_PROGRAM_ID, InvalidOracle);
```

---

## V9 — remaining_accounts Without Validation

**Detect:** `ctx.remaining_accounts.iter()` without owner, discriminator, or key checks inside the loop.

**Vulnerable:**
```rust
for account in ctx.remaining_accounts.iter() {
    let data = account.try_borrow_data()?;
    process_account_data(&data)?;  // no owner/type validation
}
```

**Exploit:** Attacker injects malicious accounts into remaining_accounts — bypass Anchor's constraint system.

**Secure:**
```rust
for account in ctx.remaining_accounts.iter() {
    require!(account.owner == &crate::ID, InvalidOwner);
    let data = account.try_borrow_data()?;
    require!(data.len() >= 8 && data[..8] == UserState::DISCRIMINATOR, InvalidType);
}
```

---

## V10 — Improper Account Closing — Revival Attack

**Detect:** Account closure via `**account.lamports.borrow_mut() = 0` without zeroing data bytes or writing `CLOSED_ACCOUNT_DISCRIMINATOR`. Missing Anchor `close = recipient` constraint.

**Vulnerable:**
```rust
**dest.lamports.borrow_mut() = dest.lamports().checked_add(source.lamports()).unwrap();
**source.lamports.borrow_mut() = 0;
// Data NOT zeroed — account can be revived by transferring lamports back in same tx
```

**Exploit:** Within same transaction: close account → transfer 1 lamport back → reuse with stale data.

**Secure:**
```rust
// Anchor: #[account(mut, close = recipient)]
// Native: zero data + drain lamports + assign to System Program
let mut data = account.try_borrow_mut_data()?;
for byte in data.deref_mut().iter_mut() { *byte = 0; }
data[..8].copy_from_slice(&CLOSED_ACCOUNT_DISCRIMINATOR);
```

---

## V11 — Operations on Closed Accounts in Same Transaction

**Detect:** Instructions that read/write accounts without checking `lamports() > 0`. Account closed in instruction N, accessed in instruction N+1 within same transaction.

**Vulnerable:**
```rust
let data = ctx.accounts.user_data.load()?;  // reads closed account — data is stale/zeroed
```

**Secure:**
```rust
require!(**ctx.accounts.user_data.to_account_info().lamports.borrow() > 0, AccountClosed);
```

---

## V12 — Duplicate Mutable Accounts

**Detect:** Two or more `#[account(mut)]` fields of the same `Account<'info, T>` type without `constraint = a.key() != b.key()`.

**Vulnerable:**
```rust
#[account(mut)] pub from: Account<'info, TokenAccount>,
#[account(mut)] pub to: Account<'info, TokenAccount>,
// from == to: self-transfer doubles balance
```

**Exploit:** Pass same account for `from` and `to`. Last serialization wins — balance doubled.

**Secure:**
```rust
#[account(mut, constraint = from.key() != to.key() @ SameAccount)]
```

---

## V13 — Missing Token Account Mint Validation

**Detect:** Token account used in transfer/CPI without `token::mint = expected_mint` constraint or manual `mint` field comparison.

**Vulnerable:**
```rust
#[account(mut)]
pub user_token: Account<'info, TokenAccount>,
// No check: user_token.mint == expected_mint.key()
```

**Exploit:** Attacker passes token account for a worthless mint, receives valuable tokens.

**Secure:**
```rust
#[account(mut, token::mint = expected_mint)]
```

---

## V14 — Missing Token Account Authority Validation

**Detect:** Token account `owner` field not validated against expected authority. Missing `token::authority = expected` or `has_one = authority`.

**Vulnerable:**
```rust
#[account(mut)]
pub vault_token: Account<'info, TokenAccount>,
// No check: vault_token.owner == vault_pda.key()
// Attacker passes their own token account
```

**Secure:**
```rust
#[account(mut, token::authority = vault_pda)]
```

---

## V15 — Sysvar Account Spoofing

**Detect:** Sysvar passed as `AccountInfo<'info>` without address validation. Use of `load_instruction_at()` (unchecked) instead of `load_instruction_at_checked()`. Absence of `Sysvar<'info, Clock>` type.

**Vulnerable:**
```rust
let instructions_sysvar = next_account_info(accounts_iter)?;
let ix = load_instruction_at(0, instructions_sysvar)?;  // unchecked — no address validation!
```

**Exploit:** **Wormhole ($320M)** — attacker passed fake Instructions sysvar, bypassed guardian signature verification.

**Secure:**
```rust
// Use syscall (no account needed): Clock::get()?
// Or validate address: require!(*sysvar.key == sysvar::instructions::ID);
// Or Anchor: Sysvar<'info, Clock>
```

---

## V16 — Instruction Introspection Bypass

**Detect:** `load_instruction_at_checked(0, ...)` with hardcoded absolute index. Same instruction at index 0 validates multiple program invocations.

**Vulnerable:**
```rust
let prev_ix = load_instruction_at_checked(0, &sysvar)?;  // absolute index 0
require!(prev_ix.program_id == ed25519_program::ID);
// Attacker puts benign Ed25519 at index 0, reuses it to validate malicious calls at index 1, 2, ...
```

**Secure:**
```rust
let current_idx = load_current_index_checked(&sysvar)?;
let prev_ix = load_instruction_at_checked((current_idx - 1) as usize, &sysvar)?;
// Relative indexing — each instruction validates its own predecessor
```

---

## V17 — System / Token Program Confusion

**Detect:** `AccountInfo<'info>` used for program accounts instead of `Program<'info, Token>` or `Program<'info, System>`. Missing `require_keys_eq!` against known program IDs.

**Vulnerable:**
```rust
pub token_program: AccountInfo<'info>,  // not validated
// invoke(&instruction, &[..., token_program.clone()])?;
```

**Exploit:** Attacker passes malicious program — returns success without performing transfer.

**Secure:**
```rust
pub token_program: Program<'info, Token>,  // auto-validates program ID
```

---

## V18 — Missing Config Account Update Constraints

**Detect:** Config/settings update instructions with `Signer<'info>` but no `constraint` linking signer to stored admin. No range validation on numeric params.

**Vulnerable:**
```rust
pub struct UpdateConfig<'info> {
    #[account(mut)]
    pub config: Account<'info, Config>,
    pub admin: Signer<'info>,  // any signer can call — no has_one!
}
```

**Secure:**
```rust
#[account(mut, has_one = admin)]
pub config: Account<'info, Config>,
pub admin: Signer<'info>,
// Plus: require!(new_fee_bps <= 10_000, FeeTooHigh);
```

---

## V19 — Predictable PDA Initialization

**Detect:** `#[account(init, ...)]` with PDA seeds that don't include the payer's/authority's key. Seeds derivable by anyone — attacker can pre-create the PDA.

**Vulnerable:**
```rust
#[account(init, seeds = [b"config", pool.key().as_ref()], bump, payer = user)]
pub config: Account<'info, Config>,
// Anyone can derive this PDA and initialize it first via Jito bundle
```

**Exploit:** **Pump Science (H-01)** — attacker front-ran `lock_pool` by pre-creating the lock_escrow PDA with predictable seeds. Legitimate migration blocked.

**Secure:**
```rust
seeds = [b"config", pool.key().as_ref(), authority.key().as_ref()]
// Or: validate caller is upgrade authority
```

---

## V20 — Missing Account Validation Chain

**Detect:** `Account<'info, T>` without `seeds`/`bump` or `address` constraint used as a trust anchor for other account constraints (e.g., `has_one`, `constraint`).

**Vulnerable:**
```rust
pub config: Account<'info, Config>,          // UNCONSTRAINED — attacker provides fake
#[account(constraint = vault.config == config.key())]
pub vault: Account<'info, Vault>,            // meaningless — validates against fake config
```

**Exploit:** **Cashio ($48M)** — fake `bank` account passed, all downstream constraints validated against it. Attacker minted unlimited stablecoins with worthless collateral.

**Secure:**
```rust
#[account(seeds = [b"config"], bump = config.bump)]  // PDA-anchored root of trust
pub config: Account<'info, Config>,
#[account(constraint = vault.config == config.key())]
pub vault: Account<'info, Vault>,
```

---

## V21 — Account Space Miscalculation

**Detect:** `space = ` in `#[account(init)]` without `8 +` (missing discriminator). Wrong sizes for `Pubkey` (32), `u64` (8), `bool` (1), `Vec<T>` (4 + len * T), `String` (4 + len), `Option<T>` (1 + T).

**Vulnerable:**
```rust
#[account(init, space = std::mem::size_of::<UserState>(), payer = user)]
// Missing 8-byte discriminator — account too small, deserialization fails
```

**Exploit:** Undersized accounts cause runtime deserialization errors → DoS. From **Mintify audit**: wrong `LEN` calculations (off by bytes).

**Secure:**
```rust
#[account(init, space = 8 + UserState::INIT_SPACE, payer = user)]
// Or: #[derive(InitSpace)] on the struct
```

---

## V22 — Ed25519 Signature Verification Bypass

**Detect:** `ed25519_program::ID` check without validating signing pubkey, message content, or signature bytes. Missing nonce for replay prevention. Absolute instruction index.

**Vulnerable:**
```rust
let ix = load_instruction_at_checked(0, &sysvar)?;
require!(ix.program_id == ed25519_program::ID);
// Doesn't validate WHICH pubkey signed or WHAT message was signed
```

**Exploit:** Attacker reuses a legitimate Ed25519 verification from another context. No replay protection — same signature works across transactions.

**Secure:**
```rust
let sig_data = Ed25519InstructionData::unpack(&ix.data)?;
require!(sig_data.public_key == expected_signer.to_bytes());
require!(sig_data.message == expected_message);
// Nonce: require!(!nonce_account.used); nonce_account.used = true;
```

---

## V23 — Unconstrained Mint Authority

**Detect:** Mint instruction without `mint::authority = expected` constraint. Token mint CPI without authority signer validation.

**Vulnerable:**
```rust
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
    token::mint_to(ctx.accounts.mint_ctx(), amount)?;
    // No check that caller is authorized to mint
}
```

**Exploit:** Anyone calls mint instruction → infinite supply inflation.

**Secure:**
```rust
#[account(constraint = mint.mint_authority == COption::Some(authority.key()))]
pub mint: Account<'info, Mint>,
pub authority: Signer<'info>,
```

---

## V24 — Insecure Initialization — No Upgrade Authority Check

**Detect:** Global `initialize` instruction callable by any signer without checking the program's upgrade authority.

**Vulnerable:**
```rust
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    ctx.accounts.global.authority = ctx.accounts.signer.key();  // any signer becomes authority
}
```

**Exploit:** **Lombard audit (M2), Onre audit (L2)** — attacker front-runs deployment, calls `initialize` first, becomes protocol admin.

**Secure:**
```rust
#[account(constraint = program.programdata_address()? == Some(program_data.key()))]
pub program: Program<'info, MyProgram>,
#[account(constraint = program_data.upgrade_authority_address == Some(authority.key()))]
pub program_data: Account<'info, ProgramData>,
```

---

## V25 — Missing State Update in Mutation Function

**Detect:** State update functions (like `update_settings`) that don't assign all fields from input params. Copy-paste errors where one field is simply omitted.

**Vulnerable:**
```rust
pub fn update_settings(&mut self, params: SettingsInput) {
    self.fee_rate = params.fee_rate;
    self.admin = params.admin;
    // MISSING: self.migration_allocation = params.migration_allocation;
    self.whitelist = params.whitelist;
}
```

**Exploit:** **Pump Science (H-02)** — `migration_token_allocation` never updatable after initialization. Admin believes they changed it but the value stays the same.

**Secure:** Verify every field in the input struct has a corresponding assignment. Write tests that round-trip all settings.

## references/attack-vectors/attack-vectors-2.md

# Attack Vectors Reference — PDA, CPI & Cross-Program Security (2/4)

> Part 2 of 5 · Vectors 26–50 of 105 total
> Covers: PDA derivation, seed security, CPI safety, invoke_signed, signer escalation, Token-2022 CPI, program validation

---

## V26 — Non-Canonical Bump Seed

**Detect:** `create_program_address` with user-supplied bump parameter instead of `find_program_address`. Bump accepted from instruction data or function args. Missing stored canonical bump reuse.

**Vulnerable:**
```rust
pub fn withdraw(ctx: Context<Withdraw>, bump: u8) -> Result<()> {
    let seeds = &[b"vault", user.key().as_ref(), &[bump]];  // user controls bump!
    let pda = Pubkey::create_program_address(seeds, ctx.program_id)?;
}
```

**Exploit:** Multiple valid bumps exist for same seeds. Attacker creates alternate PDAs, fragmenting state or bypassing PDA-based checks.

**Secure:**
```rust
// Anchor: seeds + bump auto-derives canonical bump
#[account(seeds = [b"vault", user.key().as_ref()], bump)]
// Native: let (pda, canonical_bump) = Pubkey::find_program_address(seeds, program_id);
// Store bump: vault.bump = canonical_bump;
```

---

## V27 — PDA Sharing — Missing User-Specific Seed

**Detect:** PDA `seeds` with only static strings (e.g., `seeds = [b"pool"]`) without user-specific or context-specific components. Same PDA reused across different authority domains.

**Vulnerable:**
```rust
#[account(seeds = [b"staking_pool_pda"], bump)]
pub pool: Account<'info, Pool>,
// All users share same PDA — one user's action affects everyone
```

**Exploit:** **Jet Protocol** — deposit notes PDA not derived from depositor pubkey. Any signed caller could burn another user's tokens.

**Secure:**
```rust
#[account(seeds = [b"user_pool", user.key().as_ref()], bump)]
```

---

## V28 — Seed Concatenation Collision

**Detect:** Variable-length user inputs (strings, byte slices) concatenated in PDA seeds without fixed-length encoding or delimiters.

**Vulnerable:**
```rust
seeds = [b"pool", token_name.as_bytes()]
// "poolABC" and "poolAB" + "C" could collide if seeds are concatenated
```

**Exploit:** `["AB", "C"]` and `["A", "BC"]` produce identical seed bytes → same PDA → cross-user state access.

**Secure:**
```rust
// Use fixed-length inputs (pubkeys are always 32 bytes)
seeds = [b"pool", mint_a.key().as_ref(), mint_b.key().as_ref()]
// Or hash variable-length inputs
```

---

## V29 — Seed Collision Across Account Types

**Detect:** Different PDA types (`vault`, `escrow`, `config`) using seeds without unique type prefixes. Same seed structure for different structs.

**Vulnerable:**
```rust
// Vault: seeds = [user.key().as_ref()]
// Escrow: seeds = [user.key().as_ref()]
// Same seeds → same PDA → type confusion
```

**Secure:**
```rust
seeds = [b"vault", user.key().as_ref()]   // unique prefix per type
seeds = [b"escrow", user.key().as_ref()]
```

---

## V30 — Arbitrary CPI — Unvalidated Program ID

**Detect:** `invoke()` or `invoke_signed()` where the target program is `AccountInfo<'info>` without `require_keys_eq!` against a known program ID. In Anchor: program account not typed as `Program<'info, T>`.

**Vulnerable:**
```rust
pub token_program: AccountInfo<'info>,  // anyone can pass any program
// ...
invoke(&transfer_ix, &[from, to, token_program.clone()])?;
```

**Exploit:** Attacker passes malicious program that returns success without transferring. **Sealevel attack #5**. Protocol believes transfer happened, updates state.

**Secure:**
```rust
pub token_program: Program<'info, Token>,  // auto-validates program ID
// Native: require!(*token_program.key == spl_token::ID);
```

---

## V31 — CPI Without Signer Seeds

**Detect:** `invoke()` used where PDA needs to sign (should be `invoke_signed()`). Or `invoke_signed()` with empty signer seeds `&[]`.

**Vulnerable:**
```rust
invoke(&transfer_ix, &[vault_pda, destination, token_program])?;  // vault_pda can't sign!
// Should be invoke_signed with vault PDA seeds
```

**Secure:**
```rust
let seeds = &[b"vault", &[vault.bump]];
invoke_signed(&transfer_ix, &[vault_pda, destination, token_program], &[seeds])?;
```

---

## V32 — CPI Signer Privilege Forwarding

**Detect:** User wallet (`Signer<'info>`) passed as a signer to CPI targeting an untrusted or upgradeable program. User's signing authority forwarded to third-party code.

**Vulnerable:**
```rust
let cpi_ctx = CpiContext::new(
    ctx.accounts.external_program.to_account_info(),  // untrusted program
    ExternalInstruction {
        user_wallet: ctx.accounts.user.to_account_info(),  // user's signer forwarded!
    },
);
```

**Exploit:** External program invokes System Program transfer from user's wallet, draining SOL. The user signed the outer transaction, so the signer privilege carries through CPI.

**Secure:**
```rust
// Use protocol PDA as CPI authority, never forward user signers to untrusted programs
let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.external_program.to_account_info(),
    ExternalInstruction { authority: ctx.accounts.protocol_pda.to_account_info() },
    signer_seeds,
);
// Verify balances after CPI: require!(user.lamports() >= pre_balance - max_spend);
```

---

## V33 — Post-CPI Account Not Reloaded

**Detect:** Account field access after any `cpi::` call or `invoke`/`invoke_signed` without intervening `.reload()?`. Stale in-memory data used for decisions.

**Vulnerable:**
```rust
token::mint_to(cpi_ctx, amount)?;
msg!("Supply: {}", ctx.accounts.mint.supply);  // STALE — shows pre-mint value
// Decision based on stale balance can enable double-spend
```

**Exploit:** **Watt Protocol audit** — stale reward accumulator after CPI led to incorrect reward calculations.

**Secure:**
```rust
token::mint_to(cpi_ctx, amount)?;
ctx.accounts.mint.reload()?;  // refresh from on-chain data
msg!("Supply: {}", ctx.accounts.mint.supply);  // correct
```

---

## V34 — CPI Return Value Ignored

**Detect:** `invoke()` or CPI helper without `?` operator. `Result` from CPI not propagated.

**Vulnerable:**
```rust
spl_token::instruction::transfer(token_program.key, source.key, dest.key, authority.key, &[], amount);
// Return value ignored! Transfer may have failed.
```

**Exploit:** Transfer fails silently, state updated as if it succeeded. Balance accounting desyncs.

**Secure:**
```rust
invoke(&spl_token::instruction::transfer(...)?, &[source, dest, authority])?;
// Or Anchor: token::transfer(ctx, amount)?;
```

---

## V35 — invoke_signed with Incorrect Seeds

**Detect:** `invoke_signed` seeds that don't match the PDA derivation. Stored bump not used. Seeds in wrong order.

**Vulnerable:**
```rust
let seeds = &[b"vault", &[bump]];  // missing user.key() that was in init seeds!
invoke_signed(&ix, &accounts, &[seeds])?;  // PDA mismatch — CPI fails or signs wrong account
```

**Secure:**
```rust
let seeds = &[b"vault", user.key().as_ref(), &[vault.bump]];  // matches init derivation exactly
invoke_signed(&ix, &accounts, &[seeds])?;
```

---

## V36 — Missing Token Program ID Discrimination

**Detect:** Token operations using hardcoded `spl_token::ID` when Token-2022 mints may be involved. `anchor_spl::token::transfer` instead of `anchor_spl::token_interface::transfer_checked`.

**Vulnerable:**
```rust
anchor_spl::token::transfer(cpi_ctx, amount)?;  // hardcodes legacy Token program
// Fails on Token-2022 mints — DoS or fund loss
```

**Exploit:** **Tensor NFT Marketplace** — royalty payouts failed for Token-2022 mints because legacy `transfer` was hardcoded.

**Secure:**
```rust
anchor_spl::token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
// Uses InterfaceAccount<'info, TokenAccount> + Interface<'info, TokenInterface>
```

---

## V37 — Token-2022 Transfer Hook Not Accounted For

**Detect:** `transfer_checked` CPI to Token-2022 mints without resolving and passing extra accounts required by the transfer hook extension.

**Vulnerable:**
```rust
transfer_checked(cpi_ctx, amount, decimals)?;
// Mint has transfer hook — extra accounts not passed via remaining_accounts
// Transfer reverts
```

**Exploit:** Protocol cannot transfer tokens with transfer hooks. DoS on deposits/withdrawals for affected mints.

**Secure:**
```rust
// Resolve hook accounts and pass via remaining_accounts
let hook_accounts = resolve_transfer_hook_accounts(&mint)?;
// Include in CPI
```

---

## V38 — Missing CPI Program ID Validation in Anchor

**Detect:** `/// CHECK:` on a program account instead of `Program<'info, T>`. CPI target program ID never validated.

**Vulnerable:**
```rust
/// CHECK: This is the token program
pub token_program: AccountInfo<'info>,
// No address validation — arbitrary CPI
```

**Secure:**
```rust
pub token_program: Program<'info, Token>,
// Or: #[account(address = spl_token::ID)]
```

---

## V39 — Cross-Program Reentrancy via CPI Callback

**Detect:** Program A makes CPI to untrusted program B, which calls back into program A. State partially updated before CPI — callback sees inconsistent state. Note: Solana's runtime prevents A→A reentrancy, but A→B→A is possible if different accounts are used.

**Vulnerable:**
```rust
// Program A: update partial state, then CPI to untrusted B
vault.pending_withdrawal = amount;  // partial state update
invoke(&ix_to_untrusted_b, &[...])?;
vault.balance -= amount;  // not yet executed when B calls back into A
```

**Exploit:** B calls back into A with different accounts, using the partially-updated state.

**Secure:**
```rust
// Update ALL state before CPI (checks-effects-interactions)
vault.balance -= amount;
vault.pending_withdrawal = 0;
invoke(&ix_to_untrusted_b, &[...])?;
```

---

## V40 — CPI to Upgradeable Program

**Detect:** `invoke` / `invoke_signed` targeting a program that is not immutable (upgrade authority != None). Target program can be maliciously upgraded between transactions.

**Vulnerable:**
```rust
invoke(&ix, &[target_program.clone(), ...])?;
// target_program could be upgraded to steal funds in next slot
```

**Secure:**
```rust
// Verify program is immutable: upgrade authority set to None
// Or: only CPI to known immutable programs (SPL Token, System Program)
// Or: validate upgrade authority is trusted (multisig, governance)
```

---

## V41 — Address Lookup Table Contains Signer

**Detect:** Signer pubkey included in ALT. Signer accounts must always be inline in the transaction message — ALT inclusion breaks signing validation.

**Vulnerable:**
```rust
// Client-side: putting signer in ALT
alt.entries.push(user_wallet.pubkey());  // breaks signing
```

**Secure:** Only non-signer accounts in ALT. Signer accounts always inline in transaction.

---

## V42 — Durable Nonce Not First Instruction

**Detect:** `AdvanceNonceAccount` instruction placed after index 0 in transaction using durable nonces.

**Vulnerable:**
```rust
// ix[0] = do_something, ix[1] = advance_nonce
// Nonce not advanced — transaction can be replayed
```

**Secure:** `AdvanceNonceAccount` must be instruction index 0.

---

## V43 — Token-2022 Permanent Delegate

**Detect:** Protocol accepting arbitrary mints without checking for `ExtensionType::PermanentDelegate`. Vaults/pools/escrows holding tokens with permanent delegates.

**Vulnerable:**
```rust
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    transfer_checked(cpi_ctx, amount, decimals)?;
    // No check if mint has PermanentDelegate extension
    // Delegate can transfer/burn tokens from vault at any time
}
```

**Exploit:** Permanent delegate transfers all deposited tokens out of vault without any signature from the vault authority.

**Secure:**
```rust
let mint_data = ctx.accounts.mint.to_account_info().try_borrow_data()?;
let mint_state = PodStateWithExtensions::<PodMint>::unpack(&mint_data)?;
require!(mint_state.get_extension::<PermanentDelegate>().is_err(), NoPermanentDelegates);
```

---

## V44 — Token-2022 Transfer Fee Accounting Mismatch

**Detect:** `transfer_checked` CPI followed by bookkeeping that uses the requested amount instead of actual received amount. Missing balance-before/balance-after pattern.

**Vulnerable:**
```rust
transfer_checked(user_token, vault_token, authority, amount, decimals)?;
vault.deposits[user] += amount;  // BUG: vault received (amount - transfer_fee)
```

**Exploit:** Accounting credits more than received. Over time, vault becomes insolvent — last withdrawers get nothing.

**Secure:**
```rust
let pre = vault_token.amount;
transfer_checked(user_token, vault_token, authority, amount, decimals)?;
ctx.accounts.vault_token.reload()?;
vault.deposits[user] += ctx.accounts.vault_token.amount - pre;  // actual received
```

---

## V45 — Token-2022 Non-Transferable Extension

**Detect:** Protocol assumes tokens are freely transferable without checking `ExtensionType::NonTransferable`. Soulbound tokens cannot be transferred between accounts.

**Vulnerable:**
```rust
// Vault accepts any mint — including soulbound tokens
// When user tries to withdraw, transfer fails permanently
```

**Secure:**
```rust
require!(mint_state.get_extension::<NonTransferable>().is_err(), NonTransferableNotSupported);
```

---

## V46 — Mint Close Authority — Reinitialization Bypass

**Detect:** Protocol accepting mints with `MintCloseAuthority` extension without re-validating mint properties on each interaction. Mint can be closed and re-created at same address with different extensions.

**Vulnerable:**
```rust
// Store mint pubkey on initialization
pool.mint = mint.key();
// Later: trust the mint without re-checking extensions
// Mint was closed and re-created without TransferFee extension → bypass fees
```

**Exploit:** Attacker creates mint → registers in protocol → closes mint → re-creates at same address with different extensions (no KYC, no fees, no soulbound). Old token accounts survive.

**Secure:**
```rust
// Re-validate mint extensions on every interaction, or reject mints with MintCloseAuthority
require!(mint_state.get_extension::<MintCloseAuthority>().is_err(), MintCloseAuthorityNotSupported);
```

---

## V47 — CPI Ordering — Lamports Before Completion

**Detect:** Manual lamport transfer (via `**lamports.borrow_mut()`) before CPI `close_account` completes. Violates Solana's instruction-level lamport balance invariant.

**Vulnerable:**
```rust
// Transfer lamports to bidder manually
**ctx.accounts.escrow.lamports.borrow_mut() -= refund_amount;
**ctx.accounts.bidder.lamports.borrow_mut() += refund_amount;
// Then CPI close_account on escrow — fails with UnbalancedInstruction
```

**Exploit:** **OneMind Auction audit** — auction cancellation permanently failed when bids existed. Funds locked.

**Secure:**
```rust
// Let CPI handle lamport transfers, or do manual transfers AFTER all CPIs complete
close_account(cpi_ctx)?;  // handles lamport transfer atomically
```

---

## V48 — Security Dependency Chain

**Detect:** `Account<'info, T>` without seeds/bump or address constraint used as root of trust for downstream `has_one`/`constraint` checks. Unconstrained root poisons all derived validations.

**Vulnerable:**
```rust
pub config: Account<'info, Config>,          // NO seeds, NO address constraint
#[account(constraint = vault.config == config.key())]
pub vault: Account<'info, Vault>,            // validates against fake config
#[account(constraint = position.vault == vault.key())]
pub position: Account<'info, Position>,      // cascading fake validation
```

**Exploit:** Entire validation chain is meaningless. Attacker provides crafted config → derives matching vault → accesses any position.

**Secure:**
```rust
#[account(seeds = [b"config"], bump)]  // PDA — unforgeable root
pub config: Account<'info, Config>,
```

---

## V49 — Dangling References After Account Close via CPI

**Detect:** Account data cached (deserialized) before CPI that closes the account. Cached data used after CPI — references stale/zeroed memory.

**Vulnerable:**
```rust
let balance = ctx.accounts.source.amount;  // cache
close_account_cpi(ctx)?;                    // source closed
msg!("Had balance: {}", balance);           // stale — source may be zeroed
// Worse: ctx.accounts.source.amount — dangling reference
```

**Secure:**
```rust
let balance = ctx.accounts.source.amount;
require!(balance > 0, EmptyAccount);
// Close LAST, after all reads
close_account_cpi(ctx)?;
// Do NOT read from source after close
```

---

## V50 — Account Reassignment Data Wipe

**Detect:** `account.assign(&system_program::ID)` followed by `account.assign(program_id)` — temporary ownership change zeroes account data.

**Vulnerable:**
```rust
account.assign(&system_program::ID);  // runtime zeroes data on ownership change
account.assign(ctx.program_id);        // reassign back — but data is gone
```

**Exploit:** All account state permanently destroyed. Program functions referencing this account break.

**Secure:** Never temporarily reassign account ownership. If ownership must change, backup and restore data explicitly.

## references/attack-vectors/attack-vectors-3.md

# Attack Vectors Reference — Arithmetic, Tokens & State Management (3/4)

> Part 3 of 5 · Vectors 51–75 of 105 total
> Covers: integer safety, precision loss, token operations, state lifecycle, fee logic, Token-2022 extensions

---

## V51 — Integer Overflow via Unchecked Arithmetic

**Detect:** Direct `+`, `-`, `*`, `/` operators on integer types without `checked_*` wrappers. Missing `overflow-checks = true` in `Cargo.toml [profile.release]`. Note: Anchor's default template sets `overflow-checks = true`, but verify it hasn't been removed.

**Vulnerable:**
```rust
vault.balance = vault.balance + amount;  // wraps on overflow in release mode!
// u64::MAX - 100 + 200 = 99
```

**Exploit:** Attacker deposits `u64::MAX - current_balance + desired_balance`, wraps to desired value.

**Secure:**
```rust
vault.balance = vault.balance.checked_add(amount).ok_or(ErrorCode::Overflow)?;
// Also verify: Cargo.toml [profile.release] overflow-checks = true
```

---

## V52 — Integer Underflow on Balance Subtraction

**Detect:** `balance - amount` or `balance -= amount` without prior `require!(balance >= amount)` or `checked_sub`.

**Vulnerable:**
```rust
vault.balance -= withdrawal_amount;  // wraps to u64::MAX if amount > balance
```

**Secure:**
```rust
vault.balance = vault.balance.checked_sub(amount).ok_or(ErrorCode::InsufficientFunds)?;
```

---

## V53 — Division Before Multiplication — Precision Loss

**Detect:** Division operator or `checked_div` followed by multiplication on integer types. `(a / b) * c` pattern.

**Vulnerable:**
```rust
let share_value = (user_deposit / total_supply) * price;
// If user_deposit < total_supply, division truncates to 0 → share_value = 0
```

**Exploit:** **Neodyme $2.6B disclosure** — rounding errors in lending protocol rate calculations. Attacker gets free operations when amounts truncate to zero.

**Secure:**
```rust
let share_value = user_deposit.checked_mul(price)?.checked_div(total_supply)?;
// Multiply first, then divide — preserves precision
// Use u128 for intermediate results
```

---

## V54 — Division by Zero

**Detect:** Division where divisor can be zero: `total_supply`, `pool_balance`, `shares_outstanding`, `total_staked`. Any division without prior zero check or `checked_div`.

**Vulnerable:**
```rust
let reward_per_share = total_rewards / total_staked;  // panics if total_staked == 0
```

**Exploit:** **Kamino Lend (W7)**, **Watt Protocol (H1)** — division by zero on first interaction or empty pool state. Transaction panics → DoS.

**Secure:**
```rust
let reward_per_share = total_rewards.checked_div(total_staked).unwrap_or(0);
// Or: require!(total_staked > 0, NoStakers);
```

---

## V55 — Unsafe Integer Casting — `as` Truncation and Sign Reinterpretation

**Detect:** `as u32`, `as u16`, `as u8` — narrowing casts using `as` keyword. `as u64` on `i64` — signed-to-unsigned reinterpretation. `as i64` on large `u64` — unsigned-to-signed overflow. Any `as` cast between integer types without `try_from`.

**Vulnerable:**
```rust
let amount_u32 = amount_u64 as u32;  // silently drops high bits
// 0x1_0000_0064 as u32 = 100 — attacker bypasses amount check

let price = oracle_price_feed.price;  // i64, can be negative
let value = amount * (price as u64);  // -1i64 as u64 = 18446744073709551615
```

**Secure:**
```rust
let amount_u32 = u32::try_from(amount_u64).map_err(|_| ErrorCode::CastOverflow)?;

require!(price > 0, NegativePrice);
let price_u64 = u64::try_from(price).map_err(|_| ErrorCode::InvalidCast)?;
```

---

## V56 — Rounding Direction Exploitation

**Detect:** `try_round_u64()` in share/token calculations. Rounding that favors users on both deposit AND withdraw paths. Missing directional rounding.

**Vulnerable:**
```rust
// Deposit: shares = collateral.try_div(rate)?.try_round_u64()?;  // rounds UP → user gets more
// Withdraw: tokens = shares.try_div(rate)?.try_round_u64()?;     // rounds UP → user gets more
```

**Exploit:** Repeated small deposit/withdraw cycles drain the pool by rounding profit each cycle.

**Secure:**
```rust
// Deposit: round DOWN (fewer shares for user): try_floor_u64()
// Withdraw: round DOWN (fewer tokens for user): try_floor_u64()
// Protocol always favors the pool
```

---

## V57 — First Depositor Vault Inflation Attack

**Detect:** Vault/pool with share-based accounting where first deposit has no minimum. Share calculation: `shares = deposit * total_shares / total_assets` where `total_shares` and `total_assets` start at 0.

**Vulnerable:**
```rust
let shares = if total_shares == 0 { deposit_amount } else {
    deposit_amount * total_shares / total_assets
};
// Attacker: deposit 1 (gets 1 share), then donate 1e9 tokens directly to vault
// total_assets = 1e9+1, total_shares = 1
// Victim deposits 1e9 tokens: shares = 1e9 * 1 / (1e9+1) = 0 shares!
```

**Exploit:** First depositor steals all subsequent deposits via share price inflation.

**Secure:**
```rust
// Virtual offset: start with non-zero virtual shares/assets
let shares = (deposit_amount + VIRTUAL_OFFSET) * total_shares / (total_assets + VIRTUAL_OFFSET);
// Or: require!(shares > 0, ZeroShares);
// Or: dead shares minted on initialization
```

---

## V58 — Round-Trip Profit — Deposit/Withdraw Arbitrage

**Detect:** Inconsistent rounding between deposit and withdraw paths. Test: `deposit(X) → withdraw(all) > X`.

**Vulnerable:**
```rust
// Deposit rounds UP shares, withdraw rounds UP tokens
// Each round trip: user profits a small amount
```

**Secure:** Both paths round in protocol's favor. Add round-trip invariant test.

---

## V59 — Saturating Math Misuse

**Detect:** `saturating_sub`, `saturating_add`, `saturating_mul` used in financial calculations where overflow/underflow should be an error, not silently clamped.

**Vulnerable:**
```rust
let remaining = health_factor.saturating_sub(penalty);
// If penalty > health_factor, silently returns 0 instead of reverting
// Unhealthy position treated as healthy
```

**Secure:**
```rust
let remaining = health_factor.checked_sub(penalty).ok_or(ErrorCode::Unhealthy)?;
```

---

## V60 — Price Slippage Not Enforced

**Detect:** Swap/purchase/trade functions without `min_amount_out`, `max_price`, or `expected_price` parameter. Price-sensitive operations without user-provided bounds.

**Vulnerable:**
```rust
pub fn swap(ctx: Context<Swap>, amount_in: u64) -> Result<()> {
    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;
    transfer_to_user(ctx, amount_out)?;
    // No minimum_amount_out check — sandwich attack
}
```

**Exploit:** MEV bot front-runs: manipulate price → user swaps at bad rate → back-run to profit.

**Secure:**
```rust
pub fn swap(ctx: Context<Swap>, amount_in: u64, min_amount_out: u64) -> Result<()> {
    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;
    require!(amount_out >= min_amount_out, SlippageExceeded);
}
```

---

## V61 — Lamport Balance Invariant Violation

**Detect:** Manual lamport manipulation that creates/destroys lamports. Sum of debits must equal sum of credits across all accounts in an instruction.

**Vulnerable:**
```rust
**account_a.lamports.borrow_mut() += 1000;
// No corresponding debit from another account — runtime rejects
```

**Secure:** All lamport transfers balanced. Use System Program for transfers.

---

## V62 — Rent Lamports to Arbitrary Destination

**Detect:** Account close with rent lamports transferred to user-specified destination without validation. `close = recipient` where recipient is unvalidated.

**Vulnerable:**
```rust
// Close sends rent to attacker-provided address instead of original payer
```

**Secure:**
```rust
#[account(mut, close = original_payer)]  // hardcoded or PDA-controlled destination
```

---

## V63 — Token Dust Account Poisoning

**Detect:** Token account close logic that doesn't handle non-zero dust balance. `close_account` requires zero balance.

**Vulnerable:**
```rust
// Attacker deposits 1 token → account can never be closed
// Rent permanently locked
```

**Secure:**
```rust
// Sweep dust before close, or reject deposits below dust threshold
// For Token-2022: also check withheld transfer fees via .closable()
```

---

## V64 — Fee Bypass on Alternative Code Path

**Detect:** Fee applied in normal path but not in emergency/alternative path. Multiple withdrawal/exit functions with inconsistent fee application.

**Vulnerable:**
```rust
pub fn withdraw(ctx, amount) { let fee = amount * fee_bps / 10000; transfer(amount - fee); }
pub fn emergency_withdraw(ctx, amount) { transfer(amount); }  // no fee!
```

**Secure:** Single fee calculation function used across all exit paths.

---

## V65 — Pre-Fee / Post-Fee Amount Confusion

**Detect:** Fee calculated on input amount, capacity/limit check uses post-fee amount (or vice versa). Inconsistent amount reference.

**Vulnerable:**
```rust
let fee = amount * fee_bps / 10000;
require!(amount <= vault.capacity);     // checks pre-fee
vault.balance += amount - fee;          // stores post-fee
// Vault can exceed capacity by fee amount
```

**Exploit:** **Pump Science (M-01)** — fee calculated on input amount before `apply_buy` recomputed the actual SOL amount. Last buyer pays wrong fee.

**Secure:** Consistent: either pre-fee throughout or post-fee throughout. Fee deducted atomically.

---

## V66 — Token Decimals Mismatch

**Detect:** Hardcoded decimal assumptions (e.g., `* 1_000_000` for USDC) without reading `mint.decimals`. Missing `transfer_checked` (requires decimals param).

**Vulnerable:**
```rust
let value_usd = token_amount * price / 1_000_000;  // assumes 6 decimals
// Fails for tokens with 9 decimals — off by 1000x
```

**Secure:**
```rust
let value = token_amount.checked_mul(price)?.checked_div(10u64.pow(mint.decimals as u32))?;
```

---

## V67 — Coupled State Fields Not Reset Atomically

**Detect:** Logically coupled fields (e.g., `shares_pending` + `total_shares`) where one is reset but not the other. Struct reset/close that doesn't zero all related fields.

**Vulnerable:**
```rust
user_state.shares_pending = 0;
// user_state.rewards_owed NOT reset — stale rewards claimable
```

**Exploit:** **Watt Protocol (C4)** — unstake withdrew full balance but position record remained intact. User re-stakes to multiply position.

**Secure:** Reset all coupled fields atomically in same instruction.

---

## V68 — Time Unit Mismatch — Slots vs Seconds

**Detect:** Code mixing `clock.slot` with `clock.unix_timestamp`. One part uses slots (~400ms), another uses seconds, compared directly.

**Vulnerable:**
```rust
let lock_end = clock.unix_timestamp + 86400;  // 1 day in seconds
// Later: if clock.slot > lock_end { unlock(); }  // slot number >> seconds — unlocks immediately
```

**Secure:** Single canonical time unit. Field names annotated: `_slot`, `_timestamp_secs`.

---

## V69 — Account Data Realloc Without Zero-Init

**Detect:** `realloc` with `zero = false` or `.realloc(new_size, false)`. After shrink + expand, old data from previous allocation leaks into new space.

**Vulnerable:**
```rust
#[account(mut, realloc = new_size, realloc::payer = payer, realloc::zero = false)]
// If account previously shrunk then re-expanded, old data leaks
```

**Secure:**
```rust
realloc::zero = true  // zeroes new space
```

---

## V70 — Unbounded Collection — Compute DoS

**Detect:** Loops over `Vec`, `remaining_accounts`, or linked structures without upper bound. Variable-length iteration that can exceed 200K (default) or 1.4M (max) compute units.

**Vulnerable:**
```rust
for user in ctx.remaining_accounts.iter() {
    process_user(user)?;  // unbounded — grows until compute exceeded
}
```

**Exploit:** Attacker grows collection until instruction permanently exceeds compute budget → DoS.

**Secure:**
```rust
const MAX_BATCH: usize = 32;
require!(ctx.remaining_accounts.len() <= MAX_BATCH, TooManyAccounts);
```

---

## V71 — Missing Preprocessing on Share Transfer

**Detect:** LP token or share transfer without settling pending fees/rewards on both source and destination first.

**Vulnerable:**
```rust
pub fn transfer_shares(from, to, amount) {
    from.shares -= amount;
    to.shares += amount;
    // from's pending rewards NOT settled — lost
    // to receives rewards they never earned
}
```

**Secure:** Settle rewards on both accounts before any share balance change.

---

## V72 — Token-2022 Interest-Bearing Token Not Accounted

**Detect:** Token-2022 interest-bearing extension where program reads raw `amount` instead of effective (interest-adjusted) balance.

**Vulnerable:**
```rust
let balance = token_account.amount;  // raw balance — doesn't include accrued interest
// Under-accounting if interest has accrued
```

**Secure:** Detect interest-bearing extension and calculate effective balance.

---

## V73 — Token-2022 Transfer Fee Blocks Account Close

**Detect:** `close_account` CPI guarded only by `amount == 0` without checking `.closable()` on fee/confidential transfer extensions. Withheld fees prevent closure.

**Vulnerable:**
```rust
if token_account.amount == 0 {
    close_account(cpi_ctx)?;  // FAILS if withheld_amount > 0
}
```

**Exploit:** Protocol cannot close token accounts, rent permanently locked.

**Secure:**
```rust
let state = PodStateWithExtensions::<PodAccount>::unpack(&data)?;
if let Ok(fee_state) = state.get_extension::<TransferFeeAmount>() {
    fee_state.closable()?;  // checks withheld == 0
}
// Harvest withheld fees to mint first, then close
```

---

## V74 — Floating-Point Arithmetic in Financial Logic

**Detect:** `f64`, `f32`, `as f64` in any financial calculation. Direct `==` comparison on floats. `ui_amount` from SPL Token used in arithmetic.

**Vulnerable:**
```rust
let ui_amount = token_account.amount as f64 / 10f64.powi(decimals as i32);
if ui_amount != 1.0 { return Err(InvalidAmount); }  // float comparison — unreliable
```

**Exploit:** **Solodit Escrow audit (Critical)** — f64 precision loss caused incorrect token amounts. Float comparison `!= 1.0` fails due to floating-point representation.

**Secure:**
```rust
// Use integer arithmetic with explicit scaling
let expected_amount = 10u64.pow(decimals as u32);  // 1.0 in base units
require!(token_account.amount == expected_amount, InvalidAmount);
```

---

## V75 — Lamport / SOL Denomination Confusion

**Detect:** Hardcoded lamport amounts that are wrong by factor of 1000 (1 SOL = 1_000_000_000 lamports, not 1_000_000). Constants like `LAMPORTS_PER_SOL` not used.

**Vulnerable:**
```rust
const LISTING_FEE: u64 = 1_000_000;  // intended: 1 SOL — actual: 0.001 SOL (off by 1000x)
```

**Exploit:** **Solodit Escrow audit (Critical)** — fees 1000x lower than intended due to missing 3 zeros.

**Secure:**
```rust
use solana_program::native_token::LAMPORTS_PER_SOL;
const LISTING_FEE: u64 = LAMPORTS_PER_SOL;  // 1_000_000_000
```

## references/attack-vectors/attack-vectors-4.md

# Attack Vectors Reference — Oracle, DeFi & Platform-Level (4/4)

> Part 4 of 5 · Vectors 76–100 of 105 total
> Covers: oracle security, staking/rewards, DeFi protocol patterns, compute/platform, input validation, real-world exploit patterns

---

## V76 — Stale Oracle Price

**Detect:** Oracle `publish_time`, `last_update`, `updated_at` field read but not compared against `clock.unix_timestamp`. Or: oracle price used without any timestamp check at all.

**Vulnerable:**
```rust
let price_feed = load_price_feed_from_account_info(&oracle_ai)?;
let price = price_feed.get_price_unchecked();  // no staleness check
let value = amount.checked_mul(price.price as u64)?;
```

**Exploit:** Oracle stops updating (network issues, feed decommissioned). Attacker trades at outdated favorable price. **Mango Markets** — attacker manipulated a thinly-traded oracle to inflate collateral value.

**Secure:**
```rust
let price = price_feed.get_price_no_older_than(&clock, MAX_STALENESS_SECS)?;
// Or manual: require!(clock.unix_timestamp - price.publish_time <= MAX_AGE, StaleOracle);
```

---

## V77 — Oracle Confidence Interval Not Validated

**Detect:** Pyth `price.conf` field never checked. Price used without `conf / price` ratio validation. `get_price_unchecked()` instead of `get_price_no_older_than()` with confidence check.

**Vulnerable:**
```rust
let price = price_feed.get_price_unchecked();
let value = collateral * price.price as u64;
// price.conf could be 50% of price — completely unreliable
```

**Exploit:** During volatile markets, confidence widens massively. Attacker borrows against inflated collateral when confidence is ±50% of price.

**Secure:**
```rust
let price = price_feed.get_price_no_older_than(&clock, MAX_AGE)?;
require!(
    price.conf.checked_mul(100)?.checked_div(price.price.unsigned_abs())? <= MAX_CONF_PCT,
    OracleConfidenceTooWide
);
```

---

## V78 — Oracle Status Not Checked

**Detect:** Pyth `PriceStatus` or Switchboard `AggregatorAccountData` status field not validated. Price used regardless of trading/halted status.

**Vulnerable:**
```rust
let price_account: PriceFeed = load_price_feed(&oracle)?;
// No status check — price may be from halted/unknown state
let value = amount * price_account.get_price_unchecked().price as u64;
```

**Secure:**
```rust
let price = price_feed.get_price_no_older_than(&clock, MAX_AGE)?;
// get_price_no_older_than returns error if status != Trading
// For Switchboard: require!(aggregator.check_staleness(...).is_ok());
```

---

## V79 — Fake Oracle Account — Missing Owner Validation

**Detect:** Oracle `AccountInfo` deserialized without owner check. `/// CHECK:` annotation on oracle account. Oracle account key not validated against stored config or hardcoded address.

**Vulnerable:**
```rust
/// CHECK: oracle account
pub oracle: AccountInfo<'info>,
// Attacker passes crafted account with fabricated price data
let price_feed = load_price_feed_from_account_info(&ctx.accounts.oracle)?;
```

**Exploit:** Attacker creates an account with the same data layout as a price feed, sets any price they want, passes it as the oracle.

**Secure:**
```rust
#[account(
    address = pool.oracle_address,  // stored on init
    owner = PYTH_PROGRAM_ID @ ErrorCode::InvalidOracle
)]
pub oracle: AccountInfo<'info>,
// Or: require!(*oracle.owner == pyth_solana_receiver_sdk::ID);
```

---

## V80 — On-Chain Spot Price as Valuation Source

**Detect:** Pool `reserve_a / reserve_b` or `token_account.amount` used for pricing instead of oracle. AMM `get_spot_price()` used in lending/liquidation logic.

**Vulnerable:**
```rust
let price = pool.reserve_sol.checked_div(pool.reserve_token)?;
// Spot price — manipulable within a single transaction
let collateral_value = user_tokens.checked_mul(price)?;
```

**Exploit:** **Mango Markets ($115M)** — attacker manipulated MNGO/USDC spot price on their own market to inflate collateral, then borrowed against it across all markets. Flash loan → pump spot price → borrow → repay flash loan.

**Secure:**
```rust
// Use oracle (Pyth/Switchboard) for valuation, NOT spot price
let oracle_price = get_oracle_price(&ctx.accounts.oracle, &clock)?;
let collateral_value = user_tokens.checked_mul(oracle_price)?;
// For AMMs: use TWAP, not instantaneous spot
```

---

## V81 — Retroactive Oracle Pricing

**Detect:** Settlement/close/liquidation uses current oracle price for positions opened at a different time. Position struct missing `entry_price` or `open_price` field.

**Vulnerable:**
```rust
pub fn settle_position(ctx: Context<Settle>) -> Result<()> {
    let current_price = get_oracle_price(&ctx.accounts.oracle)?;
    let pnl = (current_price - position.entry_price) * position.size;
    // But position.entry_price was never stored — it's always current_price!
}

pub fn open_position(ctx: Context<Open>, size: u64) -> Result<()> {
    position.size = size;
    // Missing: position.entry_price = get_oracle_price(&oracle)?;
}
```

**Secure:**
```rust
pub fn open_position(ctx: Context<Open>, size: u64) -> Result<()> {
    position.size = size;
    position.entry_price = get_oracle_price(&ctx.accounts.oracle)?;
    position.open_slot = clock.slot;
}
```

---

## V82 — Staking Reward Index Not Updated Before Balance Change

**Detect:** `stake()` or `unstake()` function that modifies `user.staked_amount` or `pool.total_staked` without first calling reward accumulator update. `reward_per_token` or `reward_index` calculation missing before balance mutation.

**Vulnerable:**
```rust
pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
    let pool = &mut ctx.accounts.pool;
    let user = &mut ctx.accounts.user_stake;
    // BUG: reward_per_token not updated before total_staked changes
    user.staked_amount += amount;
    pool.total_staked += amount;
    // New staker dilutes existing stakers' pending rewards
}
```

**Exploit:** Attacker stakes right before reward distribution, captures share of rewards earned entirely by others. Or: existing staker's pending rewards silently reduced when total_staked increases.

**Secure:**
```rust
pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
    let pool = &mut ctx.accounts.pool;
    let user = &mut ctx.accounts.user_stake;
    // Update global accumulator FIRST
    pool.reward_per_token += pending_rewards.checked_div(pool.total_staked)?;
    // Settle user's pending rewards BEFORE balance change
    user.pending_rewards += user.staked_amount * (pool.reward_per_token - user.last_reward_per_token);
    user.last_reward_per_token = pool.reward_per_token;
    // NOW safe to change balances
    user.staked_amount += amount;
    pool.total_staked += amount;
}
```

---

## V83 — Flash Stake/Unstake Reward Capture

**Detect:** No minimum staking duration. `unstake()` callable in same slot/transaction as `stake()`. Missing `lockup_until` or `last_stake_slot` check.

**Vulnerable:**
```rust
pub fn unstake(ctx: Context<Unstake>) -> Result<()> {
    let user = &mut ctx.accounts.user_stake;
    let rewards = calculate_rewards(user)?;
    // No lockup check — can stake and unstake in same transaction
    transfer_rewards(ctx, rewards)?;
    user.staked_amount = 0;
}
```

**Exploit:** Attacker: stake large amount → trigger reward distribution → claim rewards → unstake. All in one transaction. Zero capital commitment, full reward capture.

**Secure:**
```rust
pub fn unstake(ctx: Context<Unstake>) -> Result<()> {
    let clock = Clock::get()?;
    require!(
        clock.unix_timestamp >= user.last_stake_time + MIN_LOCKUP_SECS,
        LockupNotExpired
    );
}
```

---

## V84 — Reward Dilution via Direct Token Transfer

**Detect:** Reward calculation using `token_account.amount` (raw balance) instead of internal `total_staked` state variable. `reward_per_token = rewards / token_account.amount` pattern.

**Vulnerable:**
```rust
let total = ctx.accounts.staking_vault.amount;  // raw SPL balance
let reward_per_token = new_rewards.checked_div(total)?;
// Attacker transfers tokens directly to vault → inflates denominator → dilutes rewards
```

**Exploit:** Attacker sends tokens directly to the staking vault (not through `stake()`). `total` increases but no shares are minted. All stakers' reward rates diluted.

**Secure:**
```rust
let total = pool.total_staked;  // internal accounting, not raw balance
let reward_per_token = new_rewards.checked_div(total)?;
// Direct transfers don't affect total_staked
```

---

## V85 — Cooldown/Unstake Period Griefable

**Detect:** Unstaking cooldown resets on any deposit, including deposits from other users. `last_deposit_time` updated by non-owner. Cooldown field in shared account.

**Vulnerable:**
```rust
pub fn deposit_to_stake(ctx: Context<DepositToStake>, amount: u64) -> Result<()> {
    let stake = &mut ctx.accounts.stake_account;
    stake.amount += amount;
    stake.cooldown_start = Clock::get()?.unix_timestamp;  // resets on ANY deposit
    // Anyone can deposit dust to reset victim's cooldown
}
```

**Exploit:** Attacker deposits 1 lamport worth of tokens to victim's stake account every epoch, perpetually resetting their cooldown. Victim can never unstake.

**Secure:**
```rust
// Only reset cooldown on owner-initiated deposits
require!(ctx.accounts.depositor.key() == stake.owner, Unauthorized);
// Or: track cooldown per-deposit, not per-account
// Or: reject deposits below minimum threshold
```

---

## V86 — Self-Liquidation Profit

**Detect:** Liquidation function without `require!(liquidator != borrower)`. Liquidation bonus/discount exceeds penalty. Liquidator receives more value than position's bad debt.

**Vulnerable:**
```rust
pub fn liquidate(ctx: Context<Liquidate>, amount: u64) -> Result<()> {
    let bonus = amount * LIQUIDATION_BONUS_BPS / 10000;  // 10% bonus
    // No check that liquidator != position owner
    transfer_collateral(ctx, amount + bonus)?;
    // Self-liquidation: user pays off own debt, gets 10% bonus from protocol
}
```

**Secure:**
```rust
require!(
    ctx.accounts.liquidator.key() != ctx.accounts.position.owner,
    SelfLiquidationNotAllowed
);
```

---

## V87 — Token-2022 CPIGuard and DefaultAccountState DoS

**Detect:** Protocol performing CPI token transfers without checking for CPIGuard extension on source account. Protocol creating/receiving token accounts without checking mint's `DefaultAccountState` extension (frozen-by-default).

**Vulnerable:**
```rust
// CPIGuard: CPI transfer silently rejected
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // If user's token account has CPIGuard enabled, this CPI transfer FAILS
    token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
    // User's funds permanently stuck — can't withdraw via CPI
}

// DefaultAccountState: transfers to new ATAs fail
pub fn distribute(ctx: Context<Distribute>, amount: u64) -> Result<()> {
    // If mint has DefaultAccountState::Frozen, newly created ATAs start frozen
    token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
    // Transfer to frozen account fails — rewards/payouts stuck
}
```

**Exploit:** CPIGuard: User enables CPIGuard on their token account, then deposits into protocol. Protocol can never CPI-transfer tokens back — permanent lock. DefaultAccountState: Mint creates frozen accounts by default. Protocol creates ATA for user, attempts transfer — fails because destination is frozen.

**Secure:**
```rust
// Check CPIGuard before CPI operations
let account_data = ctx.accounts.source.to_account_info().try_borrow_data()?;
let state = StateWithExtensions::<Token2022Account>::unpack(&account_data)?;
if let Ok(cpi_guard) = state.get_extension::<CpiGuard>() {
    if bool::from(cpi_guard.lock_cpi) {
        return err!(ErrorCode::CpiGuardEnabled);  // reject or use alternative flow
    }
}

// Check DefaultAccountState on mint
let mint_data = ctx.accounts.mint.to_account_info().try_borrow_data()?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_data)?;
if let Ok(default_state) = mint_state.get_extension::<DefaultAccountState>() {
    if u8::from(default_state.state) == AccountState::Frozen as u8 {
        return err!(ErrorCode::MintCreatesFrozenAccounts);
    }
}
```

---

## V88 — Compute Budget Exhaustion DoS

**Detect:** Unbounded loops over `Vec`, `remaining_accounts`, or linked structures. Recursive calculations. Multiple CPIs in a single instruction. No `ComputeBudgetInstruction::set_compute_unit_limit`.

**Vulnerable:**
```rust
pub fn distribute_rewards(ctx: Context<Distribute>) -> Result<()> {
    for (i, account) in ctx.remaining_accounts.iter().enumerate() {
        let user: Account<UserStake> = Account::try_from(account)?;
        // Each iteration: deserialize + calculate + CPI transfer
        // 50+ users → exceeds 200K default, possibly even 1.4M max compute
        transfer_reward(&user)?;
    }
}
```

**Exploit:** Attacker grows the user set until the instruction permanently exceeds compute budget. Function becomes permanently uncallable — protocol DoS.

**Secure:**
```rust
const MAX_BATCH: usize = 10;
require!(ctx.remaining_accounts.len() <= MAX_BATCH, BatchTooLarge);
// Client-side: add ComputeBudgetInstruction::set_compute_unit_limit(400_000)
// Design: paginated processing with cursor stored on-chain
```

---

## V89 — Heap Exhaustion — 32KB Limit

**Detect:** Large `Vec` allocations, recursive data structures, unbounded deserialization (`BorshDeserialize` on variable-length types), or `Box::new` in loops. Solana programs have a 32KB heap limit.

**Vulnerable:**
```rust
pub fn process(ctx: Context<Process>, data: Vec<u8>) -> Result<()> {
    let parsed: Vec<UserRecord> = BorshDeserialize::deserialize(&mut &data[..])?;
    // 1000 UserRecords × 200 bytes = 200KB → heap exhaustion → program crash
    let mut results = Vec::with_capacity(parsed.len());
    for record in parsed { results.push(transform(record)?); }
}
```

**Exploit:** Attacker passes instruction data with many items, heap allocation exceeds 32KB, transaction panics. Permanent DoS if this function is required for protocol operation.

**Secure:**
```rust
require!(data.len() <= MAX_INPUT_SIZE, InputTooLarge);
// Process in fixed-size batches
// Use zero-copy deserialization: #[account(zero_copy)] or bytemuck
// Avoid Vec allocations — use fixed-size arrays or AccountLoader<'info, T>
```

---

## V90 — Missing Same-Asset Swap Check

**Detect:** Swap function where `input_mint == output_mint` is not rejected. `token_a_mint` and `token_b_mint` not compared.

**Vulnerable:**
```rust
pub fn swap(ctx: Context<Swap>, amount_in: u64) -> Result<()> {
    // No check: ctx.accounts.input_mint.key() != ctx.accounts.output_mint.key()
    let amount_out = calculate_output(amount_in, &ctx.accounts.pool)?;
    transfer_in(ctx, amount_in)?;
    transfer_out(ctx, amount_out)?;
    // Same-token swap: fee extracted from pool without real economic activity
}
```

**Secure:**
```rust
require!(
    ctx.accounts.input_mint.key() != ctx.accounts.output_mint.key(),
    SameAssetSwap
);
```

---

## V91 — Missing Deadline on Time-Sensitive Operations

**Detect:** Swap, trade, deposit, or any price-sensitive instruction without `deadline`, `valid_until`, or `expires_at` parameter. No `clock.unix_timestamp` comparison against a user-supplied expiry.

**Vulnerable:**
```rust
pub fn swap(ctx: Context<Swap>, amount_in: u64, min_out: u64) -> Result<()> {
    // Has slippage protection but NO deadline
    // Transaction can sit pending, execute hours later at stale conditions
    let out = calculate_output(amount_in)?;
    require!(out >= min_out, SlippageExceeded);
}
```

**Exploit:** Validator holds transaction, executes it much later when market conditions differ. Even with slippage protection, user may get the minimum acceptable amount when they could have gotten better elsewhere if the tx had failed promptly.

**Secure:**
```rust
pub fn swap(ctx: Context<Swap>, amount_in: u64, min_out: u64, deadline: i64) -> Result<()> {
    let clock = Clock::get()?;
    require!(clock.unix_timestamp <= deadline, TransactionExpired);
}
```

---

## V92 — Signature Replay Without Nonce

**Detect:** `Ed25519Program` or `Secp256k1Program` signature verification without including a nonce, sequence number, or domain separator in the signed message. Signed message missing program ID or chain context.

**Vulnerable:**
```rust
// Signed message: [amount, recipient]
let msg = [amount.to_le_bytes(), recipient.to_bytes()].concat();
verify_ed25519_signature(&signer_pubkey, &msg, &signature)?;
// Same signature valid forever — replay on every call
```

**Exploit:** Attacker captures a valid signed message, replays it indefinitely. Each replay executes the same operation (transfer, approval, etc.) without the signer's knowledge.

**Secure:**
```rust
// Include nonce + program_id + chain context in signed message
let msg = [
    amount.to_le_bytes().as_ref(),
    recipient.as_ref(),
    nonce.to_le_bytes().as_ref(),  // increment after use
    ctx.program_id.as_ref(),
].concat();
verify_ed25519_signature(&signer_pubkey, &msg, &signature)?;
user_state.nonce += 1;  // prevent replay
```

---

## V93 — On-Chain Randomness Manipulation

**Detect:** `clock.slot`, `clock.unix_timestamp`, `recent_slothashes`, or blockhash used as randomness source. Any `hash()` of predictable on-chain data used for lottery, selection, or distribution.

**Vulnerable:**
```rust
let random_seed = clock.slot.to_le_bytes();
let hash = hashv(&[&random_seed, user.key().as_ref()]);
let winner_index = u64::from_le_bytes(hash.to_bytes()[..8].try_into()?) % total_entries;
// Validator/attacker can predict slot → predict winner → only enter when they win
```

**Exploit:** Validators choose which slot to include the transaction in. Attackers can simulate outcomes for each slot and only submit when they win. **All on-chain "randomness" is predictable.**

**Secure:**
```rust
// Use Switchboard VRF or another verifiable randomness oracle
let vrf_result = VrfAccountData::new(&ctx.accounts.vrf)?.get_result()?;
require!(!vrf_result.eq(&[0u8; 32]), VrfNotResolved);
let winner_index = u64::from_le_bytes(vrf_result[..8].try_into()?) % total_entries;
```

---

## V94 — Off-Chain Validation Reliance

**Detect:** On-chain instruction handler that assumes client/frontend enforces invariants. Missing on-chain validation for constraints documented only in SDK/frontend code. Comments like "validated by client" or "frontend checks this".

**Vulnerable:**
```rust
pub fn create_order(ctx: Context<CreateOrder>, price: u64, amount: u64) -> Result<()> {
    // "Frontend validates price is within 5% of oracle" — NOT enforced on-chain
    order.price = price;
    order.amount = amount;
    // Attacker calls instruction directly with arbitrary price
}
```

**Exploit:** Attacker bypasses frontend, calls the instruction directly via CLI/SDK with any parameters. All "client-side validation" is meaningless for security.

**Secure:**
```rust
let oracle_price = get_oracle_price(&ctx.accounts.oracle)?;
require!(
    price >= oracle_price * 95 / 100 && price <= oracle_price * 105 / 100,
    PriceOutOfRange
);
```

---

## V95 — Rent Exemption Not Enforced in Bonding Curves

**Detect:** Bonding curve or treasury account that can have SOL withdrawn below rent-exempt minimum. `transfer` of SOL without checking remaining balance covers rent. Manual lamport manipulation without rent check.

**Vulnerable:**
```rust
pub fn sell(ctx: Context<Sell>, amount: u64) -> Result<()> {
    let sol_out = calculate_sell_price(amount, &ctx.accounts.bonding_curve)?;
    **ctx.accounts.bonding_curve.lamports.borrow_mut() -= sol_out;
    **ctx.accounts.seller.lamports.borrow_mut() += sol_out;
    // No check: bonding_curve may drop below rent-exempt minimum
    // Runtime garbage-collects the account → all state lost
}
```

**Exploit:** **Pump Science (M-02)** — sell operation could drain bonding curve below rent-exempt threshold. Account gets garbage collected, destroying the entire curve state and locking remaining tokens.

**Secure:**
```rust
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(ctx.accounts.bonding_curve.data_len());
require!(
    ctx.accounts.bonding_curve.lamports() - sol_out >= min_balance,
    InsufficientRentBalance
);
```

---

## V96 — Transfer Hook Validation — Fake Mint with Malicious Hook

**Detect:** Protocol accepting arbitrary Token-2022 mints with transfer hooks without validating the hook program. Mint's `TransferHook` extension points to attacker-controlled program. Missing `transferring` flag check in hook program.

**Vulnerable:**
```rust
// Protocol accepts any Token-2022 mint without checking transfer hook program
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    transfer_checked(cpi_ctx, amount, decimals)?;
    // Mint's transfer hook program could:
    // 1. Revert selectively (DoS withdrawals but not deposits)
    // 2. Execute arbitrary logic on every transfer
}
```

**Exploit:** Attacker creates mint with transfer hook pointing to malicious program. Deposits succeed, but hook program reverts on withdrawals → funds locked. Or: hook program has side effects that manipulate other protocol state.

**Secure:**
```rust
// Allowlist specific mints, or validate the hook program
if let Ok(hook) = mint_state.get_extension::<TransferHook>() {
    let hook_program = Option::<Pubkey>::from(hook.program_id);
    require!(
        hook_program.is_none() || ALLOWED_HOOK_PROGRAMS.contains(&hook_program.unwrap()),
        UnsupportedTransferHook
    );
}
```

---

## V97 — `.unwrap()` Panic in Instruction Handler

**Detect:** `.unwrap()`, `.expect()`, `panic!()`, `unreachable!()`, `todo!()` in any instruction handler code path. `array[index]` without bounds check (panics on out-of-bounds).

**Vulnerable:**
```rust
pub fn process(ctx: Context<Process>, index: u8) -> Result<()> {
    let item = ctx.accounts.pool.items[index as usize];  // panics if index >= items.len()
    let value = some_option.unwrap();  // panics if None
    let parsed: u64 = data.try_into().unwrap();  // panics on bad data
}
```

**Exploit:** Attacker passes crafted input that triggers `.unwrap()` on `None`/`Err` or out-of-bounds array access. Transaction panics with uninformative error. If this is a critical path (e.g., liquidation, withdrawal), it becomes a DoS vector.

**Secure:**
```rust
let item = ctx.accounts.pool.items
    .get(index as usize)
    .ok_or(ErrorCode::IndexOutOfBounds)?;
let value = some_option.ok_or(ErrorCode::MissingValue)?;
let parsed: u64 = data.try_into().map_err(|_| ErrorCode::InvalidData)?;
```

---

## V98 — Missing Input Amount Validation

**Detect:** Instruction accepting `amount: u64` without checking `amount > 0` or `amount <= MAX`. Zero-amount operations that trigger state changes (events, nonce increments, share minting) without economic commitment.

**Vulnerable:**
```rust
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    // No minimum check — amount = 0 is accepted
    transfer(cpi_ctx, amount)?;  // transfers 0 tokens — succeeds
    user.deposit_count += 1;     // state changed without economic action
    emit!(DepositEvent { amount });  // pollutes event log
}
```

**Exploit:** Zero-amount deposits to: trigger side effects, manipulate counters, spam events, or satisfy "has deposited" requirements without committing funds. `u64::MAX` amounts overflow subsequent calculations.

**Secure:**
```rust
require!(amount > 0, ZeroAmount);
require!(amount <= MAX_DEPOSIT, AmountTooLarge);
```

---

## V99 — Program Upgrade Authority Not Secured

**Detect:** Upgradeable program where upgrade authority is a single EOA (not multisig, not governance, not `None`). `BPFUpgradeableLoader` programdata account with single-key authority.

**Vulnerable:**
```rust
// solana program deploy --upgrade-authority <single-hot-wallet>
// Attacker compromises this one key → deploys malicious code → drains all TVL
```

**Exploit:** Highest-impact vector for upgradeable programs. Single compromised key replaces entire program logic. All funds in program-owned PDAs immediately drainable.

**Secure:**
```rust
// Option 1: Make immutable
solana program set-upgrade-authority <PROGRAM_ID> --final

// Option 2: Multisig/governance authority
solana program set-upgrade-authority <PROGRAM_ID> --new-upgrade-authority <MULTISIG_ADDRESS>

// Option 3: Timelock — users can exit before upgrade takes effect
```

---

## V100 — Interest Accrual During Protocol Pause

**Detect:** `paused` flag that gates deposits/withdrawals but not interest/fee accumulation functions. `accrue_interest()` callable (or auto-triggered) while paused. No pause check on time-dependent state updates.

**Vulnerable:**
```rust
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    require!(!pool.paused, ProtocolPaused);  // gated
    accrue_interest(&mut pool)?;  // interest updated on deposit
    // ...
}

pub fn accrue_interest(pool: &mut Pool) -> Result<()> {
    // NOT gated by pause — interest accrues during pause
    let elapsed = clock.unix_timestamp - pool.last_accrual;
    pool.accrued_interest += pool.total_borrows * rate * elapsed;
    pool.last_accrual = clock.unix_timestamp;
}
```

**Exploit:** Protocol pauses for emergency (exploit, upgrade). Interest keeps accruing. When unpaused, borrowers face unexpected interest charges. Positions liquidated immediately due to interest accumulated during pause. Lenders may also be affected if utilization-based rates spike.

**Secure:**
```rust
pub fn accrue_interest(pool: &mut Pool) -> Result<()> {
    if pool.paused {
        pool.last_accrual = clock.unix_timestamp;  // skip accrual window
        return Ok(());
    }
    // Normal accrual logic
}
// Or: cap accrued interest to pre-pause levels on unpause
```

## references/attack-vectors/attack-vectors-5.md

# Attack Vectors Reference — Additional Vectors (5/5)

> Part 5 · Vectors 101–105
> Covers: CPI ownership reassignment, unsafe deserialization, orphan accounts, partial discriminators, Token-2022 dynamic sizing

---

## V101 — Account Ownership Reassignment via CPI

**Detect:** `system_instruction::assign` or `system_instruction::allocate` in CPI where target account's signer privilege is forwarded from caller.

**Vulnerable:**
```rust
pub fn exploit(accounts: &[AccountInfo]) -> ProgramResult {
    let user_account = &accounts[0];  // signer privilege forwarded from caller
    let ix = system_instruction::assign(user_account.key, &malicious_program_id);
    invoke(&ix, &[user_account.clone()])?;  // steals ownership
    Ok(())
}
```

**Exploit:** Attacker's program receives forwarded signer privilege and reassigns account ownership. Attacker can then modify account data freely.

**Secure:**
```rust
// Never forward user signer to untrusted programs (V32)
// Post-CPI: require!(*account.owner == expected_program_id, OwnershipChanged);
```

---

## V102 — Unsafe Deserialization Without Input Length Validation

**Detect:** `BorshDeserialize::deserialize` or `try_from_slice` on user-provided data without `data.len()` validation. Trailing bytes silently ignored.

**Vulnerable:**
```rust
let input = MyInput::try_from_slice(data)?;
// Undersized: panic. Oversized: trailing bytes ignored
```

**Secure:**
```rust
require!(data.len() == EXPECTED_SIZE, InvalidInputLength);
let input = MyInput::try_from_slice(data)?;
```

---

## V103 — Orphan Account from Parent-Child Lifecycle

**Detect:** Parent account closeable while child accounts still reference it. No `active_children` counter or cascade-close logic.

**Vulnerable:**
```rust
pub fn close_pool(ctx: Context<ClosePool>) -> Result<()> {
    // UserStake accounts with seeds = [b"stake", pool.key()] still exist
    // Users can't unstake — pool gone, has_one = pool fails
    Ok(())
}
```

**Exploit:** Parent closed, child accounts orphaned. User funds locked permanently.

**Secure:**
```rust
require!(pool.active_positions == 0, PoolHasActivePositions);
```

---

## V104 — Partial Discriminator / Selector Matching

**Detect:** Instruction dispatch using single-byte discriminator (`data[0]`). Account type validation using fewer than 8 bytes.

**Vulnerable:**
```rust
match instruction_data[0] {
    0 => process_initialize(accounts, &instruction_data[1..]),
    1 => process_deposit(accounts, &instruction_data[1..]),
    // Only 256 values — collision risk
}
```

**Exploit:** Attacker crafts accounts/inputs passing partial type check with different data layout — type confusion (V3).

**Secure:**
```rust
let discriminator = &account_data[..8];
require!(discriminator == Vault::DISCRIMINATOR, WrongAccountType);
```

---

## V105 — Dynamic Token-2022 Account Size via Extensions

**Detect:** Hardcoded `space = TokenAccount::LEN` (165 bytes) for Token-2022 accounts. Missing extension size calculation.

**Vulnerable:**
```rust
#[account(init, payer = user, space = TokenAccount::LEN)]
pub vault_token: InterfaceAccount<'info, TokenAccount>,
// Token-2022 with extensions needs more space — creation fails
```

**Exploit:** Cannot create token accounts for mints with extensions. DoS on deposits/withdrawals.

**Secure:**
```rust
let space = ExtensionType::try_calculate_account_len::<Token2022Account>(&required_extensions)?;
```

## references/hacking-agents

```

```

## references/hacking-agents/access-control-agent.md

# Access Control Agent

You are an attacker that exploits permission models. Map the complete access control surface, then exploit every gap: unprotected handlers, escalation chains, broken initialization, inconsistent guards.

Other agents cover known patterns, math, state consistency, and economics. You break the permission model.

## Attack plan

**Map the permission model.** Every authority account, Anchor constraint (`has_one`, `Signer`, `seeds`, `bump`, `constraint`), manual check (`require!`, `if key != authority`), and PDA-based gating. Who grants what to whom. This map is your weapon — every attack below references it.

**Exploit missing Signer checks.** `AccountInfo` does NOT enforce signer status — only `Signer<'info>` or manual `account.is_signer` checks do. For every handler, verify that authority accounts are `Signer<'info>` (Anchor) or explicitly checked (native). If an authority is `AccountInfo` without a signer check, anyone can pass any pubkey.

**Exploit missing owner checks.** `AccountInfo` does NOT verify the account is owned by your program. Without `Account<'info, T>` (Anchor) or manual `account.owner == program_id` checks, an attacker can pass accounts from any program with crafted data. Type cosplay: forge an account with matching discriminator bytes from a different program.

**Exploit inconsistent guards.** For every state account written by 2+ handlers, find the one with the weakest guard. If `admin_set_fee` requires authority but `update_config` writes the same field unguarded — use it. Check `init_if_needed` handlers that re-initialize without checking existing state.

**Hijack initialization.** Call `initialize` before the legitimate deployer. Front-run deployment to set your own authority. Pass `Pubkey::default()` as authority to permanently lock out admins. Exploit `init_if_needed` to silently reinitialize.

**Exploit PDA authority gaps.** PDA-signed CPIs are powerful — verify the PDA seeds are sufficiently specific. Missing user-specific seeds allow any user to invoke operations meant for another. Shared PDAs where the authority PDA controls assets for multiple users — find where one user's action affects another's funds.

**Escalate privileges.** Find routes where a low-privilege role can grant itself a higher role. Chain authority transfer sequences to reach the upgrade authority. Exploit `set_authority` handlers that don't verify the current authority is signing.

**Exploit UncheckedAccount.** When `UncheckedAccount` (or `AccountInfo`) is used in Anchor, none of the automatic validation runs. Verify the handler manually validates owner, discriminator, and data layout. If not — pass a crafted account.

**Abuse program ID validation gaps.** `AccountInfo` for a program account doesn't verify it's the expected program. Without `Program<'info, T>` or manual `key == expected_program_id`, an attacker can substitute a malicious program for CPI targets.

## Output fields

Add to FINDINGs:
```
guard_gap: the guard that's missing — show the parallel handler that has it
proof: concrete call sequence achieving unauthorized access
```

## references/hacking-agents/economic-security-agent.md

# Economic Security Agent

You are an attacker that exploits external dependencies, value flows, and economic incentives. You have unlimited capital and can compose arbitrary instruction sequences in a single transaction. Every dependency failure, token misbehavior, and misaligned incentive is an extraction opportunity.

Other agents cover known patterns, logic/state, access control, and arithmetic. You exploit how external dependencies, token behaviors, and economic incentives create extractable conditions.

## Attack surfaces

**Break dependencies.** For every external dependency (Pyth/Switchboard oracle, SPL token program, CPI target), construct a failure that permanently blocks withdrawals, liquidations, or claims. Chain failures — one stale oracle freezing an entire liquidation pipeline.

**Exploit oracle weaknesses.** Pyth: ignore `publish_time` staleness, use price despite low `confidence`, exploit `expo` sign/magnitude. Switchboard: stale `latest_confirmed_round`, manipulable aggregator with few oracles. For every oracle read, check staleness threshold, confidence interval, and decimal normalization.

**Exploit token misbehavior.** Token-2022 transfer fees (actual received != requested amount), transfer hooks (arbitrary code execution on transfer), permanent delegate (someone else can drain the token account), non-transferable tokens, interest-bearing (balance changes without transfer). Find where the code uses requested amounts instead of actual received amounts.

**Extract value atomically.** Construct deposit→manipulate→withdraw in a single transaction (Solana allows multiple instructions). Sandwich every price-dependent operation. Push fee formulas to zero (free extraction) and max (overflow). Find the cheapest griefing vector that blocks other users.

**Exploit CPI trust boundaries.** When the program CPIs to an unvalidated program ID, an attacker can substitute a malicious program that returns crafted data. After CPI, in-memory account data is stale — find where the code reads account data cached before CPI without calling `reload()` or re-deserializing.

**Abuse lamport balance manipulation.** Anyone can send lamports to any account via system transfer. If the program trusts `account.lamports()` for business logic (not just rent-exemption), an attacker can manipulate it. Similarly, closing an account and sending remaining lamports can affect rent-exemption checks.

**Poison token accounts with dust.** Tiny deposits prevent token account closure (non-zero balance). Use this to grief users who need to close accounts, or to keep PDA token accounts alive for revival attacks.

**Starve shared capacity.** When multiple accounting variables share a cap (total deposits across vaults, global borrow limits), consume all capacity with one to permanently block the other.

**Weaponize protocol mechanisms.** Use the protocol's own features against it: stake to manipulate voting power, deposit to block liquidation thresholds, trigger intentional CPI failures to corrupt state.

**Every finding needs concrete economics.** Show who profits, how much, at what cost. No numbers = LEAD.

## Output fields

Add to FINDINGs:
```
proof: concrete numbers showing profitability or fund loss
```

## references/hacking-agents/execution-trace-agent.md

# Execution Trace Agent

You are an attacker that exploits execution flow — tracing from entry point to final state through serialization, account data, branching, CPI calls, and state transitions. Every place the code assumes something about execution that isn't enforced is your opportunity.

Other agents cover known patterns, arithmetic, permissions, economics, invariants, periphery, and first-principles. You exploit **execution flow** across instruction and transaction boundaries.

## Within a transaction

- **Parameter divergence.** Feed mismatched inputs: claimed amount != actual transferred amount, specified token mint != actual token account mint. Find every handler with 2+ attacker-controlled inputs and break the assumed relationship between them.
- **Value leaks.** Trace every value-moving handler from entry to final transfer CPI. Find where fees are deducted from one variable but the original amount is passed to `transfer`. Deposit token A, specify token B in instruction data, drain the program's B balance.
- **Serialization mismatches.** Exploit Borsh deserialization order assumptions, manual byte parsing with wrong offsets, `try_from_slice` on untrusted data without length validation. Custom serialization that reads fields in a different order than they were written.
- **Sentinel bypass.** `Pubkey::default()`, `0_u64`, `u64::MAX`, empty `Vec<u8>` trigger special paths. Find where the special path skips validation the normal path enforces.
- **Post-CPI stale data.** After a CPI call, the in-memory `AccountInfo` data may be stale — the called program may have modified the account. Find where code reads account fields after CPI without calling `reload()` (Anchor) or re-deserializing from `account.data`. This is Solana's equivalent of reentrancy.
- **CPI return values ignored.** `invoke()` and `invoke_signed()` return `ProgramResult` — find where the return value is silently discarded (no `?` operator). A failed CPI that doesn't propagate the error leaves state inconsistent.
- **Remaining accounts injection.** When a handler iterates `ctx.remaining_accounts`, an attacker controls what accounts are passed. Find where remaining accounts are used without validating owner, key, or data layout.
- **Partial state updates.** Find handlers that update coupled state variables but can error mid-update (after some writes but before others). On Solana, failed transactions roll back, but CPI failures caught with match/if-let don't roll back the outer instruction's state changes.

## Across transactions

- **Wrong-state execution.** Execute handlers in program states they were never designed for — call `withdraw` before `initialize`, `claim_rewards` before `deposit`, `close` while funds are still locked.
- **Account closing and revival.** Close an account (zero lamports, reassign owner to system program), then in the same transaction, send lamports back to revive it with stale/zeroed data. The program may re-read it as valid.
- **Operation interleaving.** Corrupt multi-step operations (request → wait → execute) by acting between steps. Front-run the execute step with a state change that makes the cached request parameters stale.
- **Instruction introspection bypass.** If the program uses `sysvar::instructions` to verify it's called in a specific context (e.g., after a flash loan repay), construct a transaction that satisfies the introspection check while still exploiting the program.
- **Address Lookup Table manipulation.** ALTs resolve at transaction load time. If the program trusts account ordering, verify that ALT-resolved accounts maintain expected positions and identities.
- **Durable nonce ordering.** Transactions using durable nonces can be delayed and executed later when state has changed. Find where time-sensitive operations don't validate freshness beyond the nonce.

## Output fields

Add to FINDINGs:
```
input: which parameter(s)/account(s) you control and what values you supply
assumption: the implicit assumption you violated
proof: concrete trace from entry to impact with specific values
```

## references/hacking-agents/first-principles-agent.md

# First Principles Agent

You are an attacker that exploits what others can't even name. Ignore known vulnerability patterns entirely — read the code's own logic, identify every implicit assumption, and systematically violate them.

Other agents scan for known patterns, arithmetic, access control, economics, state transitions, and data flow. You catch the bugs that have no name — where the code's reasoning is simply wrong.

## How to attack

**Do not pattern-match.** Forget "missing signer check" and "oracle manipulation." For every line, ask: "this assumes X — break X."

For every state-changing handler:

1. **Extract every assumption.** Values (balance is current, price is fresh, account exists), ordering (initialize ran before deposit, deposit before withdraw), identity (this pubkey is who we think, this account is owned by our program), arithmetic (fits in type, nonzero denominator, no overflow), state (PDA exists, flag was set, no concurrent modification by another instruction in the same tx).

2. **Violate it.** Find who controls the inputs. Construct multi-instruction transaction sequences that reach the handler with the assumption broken. On Solana, multiple instructions in one transaction share account state — exploit cross-instruction assumptions.

3. **Exploit the break.** Trace execution with the violated assumption. Identify corrupted account data and extract value from it.

## Focus areas

- **Stale reads.** Read account data, modify it via CPI or another instruction, reuse the now-stale value — exploit the inconsistency.
- **Desynchronized coupling.** Two account fields (or two separate accounts) must stay in sync. Find the handler that updates one but not the other.
- **Boundary abuse.** Zero, u64::MAX, first call, last participant, empty account, supply of 1 — find where the code degenerates.
- **Cross-handler breaks.** Handler A leaves state in configuration X. Find where handler B mishandles X.
- **Assumption chains.** Handler A assumes handler B validated. Handler B assumes handler A pre-validated. Neither checks — exploit the gap.
- **Account ownership assumptions.** Code assumes an AccountInfo is owned by a specific program without checking. Code assumes a PDA was derived with specific seeds without re-deriving.
- **CPI assumptions.** Code assumes a CPI target behaves correctly (returns expected data, doesn't modify unexpected accounts). Substitute a malicious program.
- **Signer assumptions.** Code assumes that because account X signed, account Y must be authorized. Break the assumed relationship between accounts.

Do NOT report named vulnerability classes, compute-unit optimizations, style issues, or admin-can-rug without a concrete mechanism.

## Output fields

Add to FINDINGs:
```
assumption: the specific assumption you violated
violation: how you broke it
proof: concrete trace showing the broken assumption and the extracted value
```

## references/hacking-agents/invariant-agent.md

# Invariant Agent

You are an attacker that exploits broken invariants — conservation laws, state couplings, and equivalence relationships. Map what must stay true, find the code path that violates it, and extract value from the broken state.

Other agents trace execution, check arithmetic, verify access control, analyze economics, scan patterns, audit periphery, and question assumptions. You break invariants.

## Step 1 — Map every invariant

Extract every relationship that must hold:

- **Lamport conservation.** The sum of all lamports across all accounts in a transaction is constant (Solana runtime enforces this). But within program logic: tracked balances must equal actual token account balances. `vault.total_deposited == token_account.amount` at all times.
- **Token supply invariants.** `mint.supply == sum(all token_account.amount for that mint)`. When the program tracks shares or receipt tokens, the internal accounting must match the SPL mint supply.
- **PDA derivation invariants.** A PDA derived with specific seeds must always resolve to the same address. Canonical bump must be stored and reused — using `find_program_address` every time is safe but using a user-supplied bump is not. Seeds must be unique per entity (user, vault, epoch) — shared seeds mean shared authority.
- **Account space invariants.** Anchor accounts have 8-byte discriminator + data. Reallocations must preserve existing data. Account size must accommodate all fields including dynamically-sized ones (Vec, String).
- **State couplings.** When X changes, Y must change too. Find all writers of X and identify which ones forget to update Y. Common: `last_update_timestamp` not refreshed when rewards are claimed, `total_staked` not decremented when a user is slashed.
- **Capacity constraints.** For every `require!(value <= limit)`, find ALL paths that increase `value`. Identify paths that skip the check.
- **Interface guarantees.** Find where view/query functions promise values that state-changing handlers fail to honor.

## Step 2 — Break each invariant

- **Break round-trips.** Make `deposit(X) → withdraw(all)` return more than X. Test with 1 lamport, u64::MAX, first/last deposit.
- **Exploit path divergence.** Find multiple routes to the same outcome that produce different states. Take the profitable path.
- **Break commutativity.** `A.deposit → B.deposit` vs `B.deposit → A.deposit` produces different state. Control ordering for extraction.
- **Abuse boundaries.** Zero balance, max capacity, first/last participant, empty state — find where invariants degenerate.
- **Bypass cap enforcement.** Enumerate ALL paths modifying a capped value — deposits, fee accrual, admin operations, emergency mode. Find the path that skips the check.
- **Exploit emergency transitions.** Break invariants during transition into or out of paused/emergency mode. Find value stranded by incomplete cleanup.

## Step 3 — Construct the exploit

For every broken invariant: what initial state is needed, what calls break it, what call extracts value, who loses.

## Output fields

Add to FINDINGs:
```
invariant: the specific conservation law, coupling, or equivalence you broke
violation_path: minimal sequence of calls that breaks it
proof: concrete values showing invariant holding before and broken after
```

## references/hacking-agents/math-precision-agent.md

# Math Precision Agent

You are an attacker that exploits integer arithmetic: rounding errors, precision loss, decimal mismatches, overflow, unsafe casts, and scale mixing. Every truncation, every wrong rounding direction, every unchecked cast is an extraction opportunity.

Other agents cover logic, state, and access control. You exploit the math.

## Attack surfaces

**Map the math.** Identify all fixed-point systems (basis points, token decimals, oracle price scales, reward accumulators), scale conversion points, and every division in value-moving handlers.

**Exploit wrong rounding.** Deposits must round shares DOWN, withdrawals round assets DOWN, debt rounds UP, fees round UP. Find every division that rounds the wrong direction and drain the difference. Compoundable wrong direction = critical.

**Zero-round to steal.** Feed minimum inputs (1 lamport, 1 share) into every calculation. Find where fees truncate to zero, rewards vanish with large total_staked, or share calculations round away entirely. A ratio truncating to zero flips formulas — exploit it.

**Amplify truncation.** Find division-before-multiplication chains — intermediate truncation amplified by later multiplication. Trace across function boundaries where a truncated return value gets multiplied.

**Exploit `as` truncation.** Rust `as` silently truncates: `u128 as u64`, `u64 as u32`, `i64 as u64` (sign flip). For every `as` cast in value-moving code, construct realistic values that overflow the target type. This is Solana's most common arithmetic bug class.

**Abuse saturating math.** `saturating_sub` and `saturating_mul` hide errors by clamping instead of panicking. Find where `saturating_sub(amount)` returns 0 instead of reverting, allowing free withdrawals or zero-fee operations.

**Detect unchecked arithmetic.** Check `Cargo.toml` for `overflow-checks = false` in release profile. If disabled, all standard `+`, `-`, `*` can wrap silently. Even if enabled, `wrapping_*` methods bypass overflow checks explicitly.

**Avoid f64/f32 in financial logic.** Floating-point is non-deterministic across validators. Find any `f64`/`f32` used in balance, price, or share calculations — the result may differ between validators, breaking consensus or enabling extraction.

**Mismatch decimals.** SOL has 9 decimals, USDC has 6, some SPL tokens have 0-9. Exploit hardcoded `1e9` on 6-decimal tokens. Feed variable oracle decimals into code assuming constant decimals. Lamport/SOL confusion (1 SOL = 1e9 lamports, not 1e6).

**Inflate share prices.** As the first depositor, donate tokens to inflate the exchange rate. Make subsequent depositors round to 0 shares and steal their deposits.

**Every finding needs concrete numbers.** Walk through the arithmetic with specific values. No numbers = LEAD.

## Output fields

Add to FINDINGs:
```
proof: concrete arithmetic showing the bug with actual numbers
```

## references/hacking-agents/periphery-agent.md

# Periphery Agent

You are an attacker that exploits the code nobody else is looking at — utility modules, math libraries, helper functions, serialization code, seed derivation helpers, and shared validation logic. Core instruction handlers trust this code implicitly. One bug in a 20-line utility function compromises every caller.

## Prioritization

Target the smallest modules first. Math utilities (`math.rs`, `utils.rs`), seed/PDA derivation helpers, account validation helpers, serialization/deserialization code (custom Borsh implementations, manual byte parsing), and shared state management modules are your primary attack surface.

## Attack surfaces

For every public/pub(crate) function in target modules:

- **Exploit unvalidated inputs.** Find inputs accepted without validation and trace what a caller blindly trusts. If the handler assumes the helper validates — verify it actually does.
- **Corrupt return values.** Return zero when non-zero is expected, truncated values from `as` casts, wrong Pubkey from seed derivation. Every caller trusting this return value inherits the bug.
- **Exploit hidden state side effects.** Find account modifications, lamport transfers, or CPI calls in helpers that callers don't account for.
- **Break edge cases.** Find partial implementations that work on the happy path. Trigger the edge case that breaks them — zero inputs, max values, empty vectors, accounts with minimum rent-exempt balance.
- **Exploit serialization bugs.** Custom `pack`/`unpack` implementations that read wrong byte ranges, Borsh implementations that skip fields, `try_from_slice` on data shorter than expected. Adjacent field corruption from wrong offset arithmetic.
- **Abuse PDA derivation helpers.** Seed derivation functions that don't include all necessary components (user pubkey, mint, epoch). Bump not stored/verified. Seeds that collide across different entity types due to missing type prefixes.
- **Brick via compute exhaustion.** Find loops in utility functions whose worst-case iteration count exceeds Solana's 200k compute unit budget for the calling handler. Especially: iterating over unbounded vectors, nested PDA derivations, or recursive account traversals.
- **Exploit external CPI wrappers.** Helper functions that wrap `invoke`/`invoke_signed` — verify they validate the target program ID, check return values, and don't pass through attacker-controlled accounts without validation.

## references/hacking-agents/shared-rules.md

# Shared Scan Rules

## Reading

Your bundle has two sections:

1. **Core source** (inline) — read in parallel chunks (offset + limit), compute offsets from the line count in your prompt.
2. **Peripheral file manifest** — file paths under `# Peripheral Files (read on demand)`. Read only those relevant to your specialty.

When matching function names, check both the Rust function name and the Anchor instruction handler name (which may differ via `#[instruction]` or snake_case convention). For native programs, check `process_instruction` dispatch arms and the individual handler functions.

## Cross-program patterns

When you find a bug in one instruction handler, **weaponize that pattern across every other handler and module in the bundle.** Search by function name AND by code pattern. Finding missing signer validation in `deposit()` means you check every other handler's account validation — missing a repeat instance is an audit failure.

After scanning: escalate every finding to its worst exploitable variant (DoS may hide fund theft). Then revisit every function where you found something and attack the other branches.

## Do not report

Admin-only functions doing admin things (guarded by `has_one`, `Signer` constraint on authority, or manual `key == stored_authority` checks). Standard Anchor safety features (8-byte discriminators, automatic owner checks on `Account<'info, T>`). Self-harm-only bugs. "Authority can rug" without a concrete mechanism. Missing event emissions (Anchor `emit!`). Compute-unit micro-optimizations.

## Output

Return structured blocks only — no preamble, no narration. Exception: vector scan agent outputs its classification block first.

FINDINGs have concrete, unguarded, exploitable attack paths. LEADs have real code smells with partial paths — default to LEAD over dropping.

**Every FINDING must have a `proof:` field** — concrete values, traces, or state sequences from the actual code. No proof = LEAD, no exceptions.

**One vulnerability per item.** Same root cause = one item. Different fixes needed = separate items.

```
FINDING | program: Name | handler: func | bug_class: kebab-tag | group_key: Program | handler | bug-class
path: caller → handler → state change → impact
proof: concrete values/trace demonstrating the bug
description: one sentence
fix: one-sentence suggestion

LEAD | program: Name | handler: func | bug_class: kebab-tag | group_key: Program | handler | bug-class
code_smells: what you found
description: one sentence explaining trail and what remains unverified
```

The `group_key` enables deduplication: `ProgramName | handlerName | bug_class`. Agents may add custom fields.

## references/hacking-agents/solana-protocol-agent.md

# Solana Protocol Analysis Agent Instructions

You are a DeFi protocol security specialist analyzing Solana programs. Instead of scanning for known patterns, you classify the protocol type and run domain-specific checklists.

## Critical Output Rule

You communicate results back ONLY through your final text response. Do not output findings during analysis. Collect all findings internally and include them ALL in your final response message. Your final response IS the deliverable. Do NOT write any files — no report files, no output files. Your only job is to return findings as text.

## Workflow

1. Read all in-scope `.rs` files, plus `judging.md` and `report-formatting.md` from the reference directory provided in your prompt, in a single parallel batch.
2. **Classify the protocol type.** Determine which category (or categories) the codebase falls into. A protocol may span multiple categories.
3. **Run the relevant checklist(s)** below. For each checklist item, determine if the codebase implements it. If not, and the omission is exploitable, apply the FP gate from `judging.md`. Only findings that pass all three FP checks get reported.
4. Your final response message MUST contain every finding **already formatted per `report-formatting.md`**. Use placeholder sequential numbers.
5. If you find NO findings, respond with "No findings."

---

## Protocol Checklists

### Lending / Borrowing (14 items)

1. Health factor calculation includes accrued (not just principal) interest
2. Liquidation incentive (bonus) covers transaction cost for minimum-size positions
3. Self-liquidation is not profitable (bonus < penalty)
4. Collateral withdrawal blocked when position is underwater
5. Interest accrual paused when protocol operations are paused
6. Liquidation math handles multi-decimal tokens correctly (e.g., SOL 9 decimals, USDC 6 decimals)
7. Oracle price includes confidence interval check and staleness check
8. Bad debt socialization mechanism exists (what happens when collateral < debt?)
9. Interest rate model doesn't allow rates to overflow u64/u128 at extreme utilization
10. Borrow cap enforced per-asset and globally
11. Flash loan interaction: can a user borrow, manipulate oracle, then liquidate in one tx?
12. Partial liquidation doesn't leave dust positions that are unliquidatable
13. Collateral factor updates don't retroactively liquidate existing positions
14. Reserve factor (protocol fee on interest) deducted correctly from lender yield

### AMM / DEX (10 items)

1. Slippage parameter from user calldata, not from on-chain pool state
2. Deadline parameter present and enforced (`require!(clock.unix_timestamp <= deadline)`)
3. Multi-hop swap: slippage protection on final output, not intermediate steps
4. LP value calculated from tracked reserves, not raw token account balance
5. Fee tier not hardcoded — uses the pool's configured fee
6. Constant product (or invariant) verified after every swap
7. Flash swap callback authorized (only pool can call back)
8. Single-sided liquidity add doesn't bypass fee accounting
9. Minimum liquidity locked on pool creation (prevents empty pool manipulation)
10. Price impact check prevents trades that move price beyond threshold

### Vault / Token-Based Accounting (10 items)

1. First-depositor inflation mitigated (virtual shares, minimum deposit, dead shares)
2. Rounding direction correct: deposits round DOWN (fewer shares), withdrawals round UP (fewer tokens)
3. Round-trip (deposit → immediate withdraw) is not profitable
4. Share price not manipulable via direct token transfer to vault
5. Withdraw cannot take more than depositor's proportional share
6. Vault balance accounting uses internal tracking, not raw `token_account.amount`
7. Rebase/interest-bearing tokens handled if supported
8. Emergency withdraw path still enforces share accounting
9. Vault total supply correctly updated on every deposit/withdraw
10. Zero-share mint prevented (`require!(shares > 0)`)

### Staking / Rewards (10 items)

1. `rewardPerToken` (or equivalent accumulator) updated before any balance change
2. No flash deposit/withdraw reward capture (minimum staking duration or time-weighted)
3. Precision loss in reward calculation doesn't zero out small stakers
4. Cooldown period not griefable by dust deposits from others
5. Reward token transfer uses actual transferred amount (accounts for transfer fees)
6. Direct transfer to reward pool doesn't inflate reward rate
7. Unstake returns correct amount (considers any slashing or penalties)
8. Multiple reward tokens each have independent accumulators
9. Reward rate update doesn't retroactively change earned rewards
10. Staking position transfer settles rewards on both source and destination

### Bridge / Cross-Chain (9 items)

1. Message replay protection (nonce, hash-based dedup)
2. Source chain and sender validated (no message from unauthorized source)
3. Rate limits on bridged amounts (per-tx and per-period)
4. Decimal conversion between chains handles all token decimal combinations
5. Supply invariant: minted on destination ≤ locked on source
6. Message finality: action only taken after sufficient confirmations
7. Bridge pause mechanism with immediate effect
8. Relayer/validator diversity (not single point of failure)
9. Fee accounting: bridge fees don't create accounting discrepancy

### Governance (6 items)

1. Vote weight snapshot from past slot/block (not current — prevents flash-vote)
2. Timelock between proposal passage and execution
3. Quorum calculated from total supply, not just circulating
4. No double-voting via token transfer between wallets
5. Proposal execution restricted to passed + timelocked proposals
6. Emergency actions bypass timelock only with sufficient threshold

### Proxy / Upgradeable (8 items)

1. Upgrade authority is multi-sig or governance (not single key)
2. Upgrade has timelock or delay
3. Storage layout append-only between upgrades
4. Initialization function callable only once (`init` constraint or `is_initialized` flag)
5. `_disableInitializers` equivalent for implementation accounts
6. Verifiable build deployed (deterministic, auditable)
7. Upgrade authority transferable only via two-step process
8. Program data account properly secured

### Account Abstraction / Session Keys (7 items)

1. Session key has bounded permissions (max amount, allowed instructions, expiry)
2. Session key revocable by the account owner
3. Session key signature validates against stored key, not arbitrary
4. Replay protection on session key transactions (nonce)
5. Session key cannot upgrade itself or extend its own permissions
6. Account recovery mechanism doesn't bypass session key revocation
7. Fee payment (if delegated) has maximum bound per transaction

## references/hacking-agents/vector-scan-agent.md

# Vector Scan Agent

You are an attacker that exploits known attack vectors. Armed with your vector bundle, grind through every one, find every manifestation in this codebase, and exploit it.

## How to attack

For each vector, extract the root cause and hunt ALL manifestations — different names, account types, structures. A "stale cached account data" vector applies wherever code caches cross-program state or reads an account before CPI and uses it after.

- Construct AND concept both absent → skip
- Guard unambiguously blocks the attack → skip
- No guard, partial guard, or guard that might not cover all paths → investigate and exploit

For every vector worth investigating, trace the full attack path: confirm reachability, follow cross-instruction interactions, find the gap that lets you through.

## Break guards

A guard only stops you if it blocks ALL paths. Find the way around:
- Reach the same state through a handler without the guard
- Feed account or argument values that slip past the Anchor constraint or `require!`
- Exploit checks positioned after CPI calls (too late — account data may have changed)
- Enter through CPI callbacks, remaining_accounts injection, or instruction introspection
- Bypass PDA-gated functions by controlling seed inputs

## Output gate

Your response MUST begin with the vector classification block:

```
Skip: V1,V2,V5
Drop: V4,V9
Investigate: V3,V7
Total: 7 classified
```

Every vector in exactly one category. `Total` matches vector count. After the classification block, output FINDING and LEAD blocks.

## references/judging.md

# Finding Validation

Every finding passes four sequential gates. Fail any gate → **rejected** or **demoted** to lead. Later gates are not evaluated for failed findings.

## Gate 1 — Refutation

Construct the strongest argument that the finding is wrong. Find the Anchor constraint, `require!`, manual check, or PDA-gated path that kills the attack — quote the exact line and trace how it blocks the claimed step.

- Concrete refutation (specific guard blocks exact claimed step) → **REJECTED** (or **DEMOTE** if code smell remains)
- Speculative refutation ("probably wouldn't happen") → **clears**, continue

## Gate 2 — Reachability

Prove the vulnerable state exists in a live deployment.

- Structurally impossible (enforced invariant, Anchor constraint prevents it) → **REJECTED**
- Requires privileged actions outside normal operation → **DEMOTE**
- Achievable through normal usage or common token behaviors → **clears**, continue

## Gate 3 — Trigger

Prove an unprivileged actor executes the attack.

- Only trusted roles (authority, admin, upgrade authority) can trigger → **DEMOTE**
- Costs exceed extraction → **REJECTED**
- Unprivileged actor triggers profitably → **clears**, continue

## Gate 4 — Impact

Prove material harm to an identifiable victim.

- Self-harm only → **REJECTED**
- Dust-level, no compounding → **DEMOTE**
- Material loss to identifiable victim → **CONFIRMED**

## Confidence

Start at **100**, deduct: partial attack path **-20**, bounded non-compounding impact **-15**, requires specific (but achievable) state **-10**. Confidence >= 80 gets description + fix. Below 80 gets description only.

## Safe patterns (do not flag)

- Anchor 8-byte discriminators (automatic type safety)
- `has_one` constraints on authority fields (ownership check)
- `seeds` + `bump` with canonical bump stored in account (PDA validation)
- `checked_add`/`checked_sub`/`checked_mul` with error propagation
- `Account<'info, T>` with correct type (automatic owner + discriminator check)
- `Signer<'info>` on authority accounts (signer enforcement)
- Two-step authority transfer pattern
- Consistent protocol-favoring rounding unless compounding or zero-rounding
- `close = destination` constraint (proper account closing)

## Lead promotion

Before finalizing leads, promote where warranted:

- **Cross-handler echo.** Same root cause confirmed as FINDING in one handler → promote in every handler where the identical pattern appears.
- **Multi-agent convergence.** 2+ agents flagged same area, lead was demoted (not rejected) → promote to FINDING at confidence 75.
- **Partial-path completion.** Only weakness is incomplete trace but path is reachable and unguarded → promote to FINDING at confidence 75, description only.

## Leads

High-signal trails for manual investigation. No confidence score, no fix — title, code smells, and what remains unverified.

## Do Not Report

Linter/compiler issues, compute-unit micro-opts, naming, doc comments. Admin privileges by design (`has_one = authority`). Missing event emissions (`emit!`). Centralization without exploit path. Implausible preconditions (but Token-2022 transfer fees, transfer hooks, interest-bearing ARE plausible for programs accepting arbitrary tokens).

## references/report-formatting.md

# Report Formatting

## Report Path

Save the report to `assets/findings/{project-name}-solana-ai-audit-report-{timestamp}.md` where `{project-name}` is the repo root basename and `{timestamp}` is `YYYYMMDD-HHMMSS` at scan time.

## Output Format

````
# Security Review — <ProgramName or repo name>

---

## Scope

|                                  |                                                        |
| -------------------------------- | ------------------------------------------------------ |
| **Mode**                         | ALL / default / filename                               |
| **Framework**                    | Anchor / Native Rust / Pinocchio                       |
| **Files reviewed**               | `File1.rs` · `File2.rs`<br>`File3.rs` · `File4.rs`    | <!-- list every file, 3 per line -->
| **Confidence threshold (1-100)** | N                                                      |

---

## Findings

[95] **1. <Title>**

`program::instruction_handler` · Confidence: 95

**Description**
<The vulnerable code pattern and why it is exploitable, in 1 short sentence>

**Fix**

```diff
- vulnerable line(s)
+ fixed line(s)
```
---

[82] **2. <Title>**

`program::instruction_handler` · Confidence: 82

**Description**
<The vulnerable code pattern and why it is exploitable, in 1 short sentence>

**Fix**

```diff
- vulnerable line(s)
+ fixed line(s)
```
---

< ... all above-threshold findings >

---

[75] **3. <Title>**

`program::instruction_handler` · Confidence: 75

**Description**
<The vulnerable code pattern and why it is exploitable, in 1 short sentence>

---

< ... all below-threshold findings (description only, no Fix block) >

---

Findings List

| # | Confidence | Title |
|---|---|---|
| 1 | [95] | <title> |
| 2 | [82] | <title> |
| 3 | [75] | <title> |

---

## Leads

_Vulnerability trails with concrete code smells where the full exploit path could not be completed in one analysis pass. These are not false positives — they are high-signal leads for manual review. Not scored._

- **<Title>** — `program.handler` — Code smells: <missing guard, unsafe arithmetic, etc.> — <1-2 sentence description of the trail and what remains unverified>
- **<Title>** — `program.handler` — Code smells: <...> — <1-2 sentence description>

---

> This review was performed by an AI assistant. AI analysis can never verify the complete absence of vulnerabilities and no guarantee of security is given. Team security reviews, bug bounty programs, and on-chain monitoring are strongly recommended. Attribution: audit workflow lineage from [pashov/skills](https://github.com/pashov/skills), adapted for Solana.

````

**Rules:** Follow the template above exactly. Sort findings by confidence (highest first). Findings below the threshold get a description but no **Fix** block. Draft findings directly in report format — do not re-generate.

