# solana-auditor

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

- **Kind:** skill
- **Source:** https://github.com/Frankcastleauditor/solana-auditor-skills
- **Page:** https://forefy.com/skills/cdde93aa-c7e6-499f-acea-9098bd89b925
- **API (JSON + files):** https://forefy.com/api/asr/cdde93aa-c7e6-499f-acea-9098bd89b925

---

## README.md

# Solana Auditor

The ultimate AI-powered security audit skill for Solana — 120 attack vectors, 4 parallel scan agents, adversarial reasoning, and DeFi protocol analysis.

Built for:

- **Solana devs** who want a security check before every commit
- **Security researchers** looking for fast wins before a manual review
- **Auditors** who want systematic vector coverage as a first pass

Not a substitute for a formal audit — but the most comprehensive AI check you can run on Solana programs.

## What's Inside

- **120 attack vectors** across 4 reference files — covering account validation, PDA security, CPI trust boundaries, arithmetic safety, token operations (SPL + Token-2022), state lifecycle, oracle manipulation, DeFi protocol economics, and more
- **4 parallel vector-scan agents** — each assigned ~30 vectors, scanning the full codebase simultaneously
- **Adversarial reasoning agent** (DEEP mode) — free-form exploit hunting using Feynman questioning, state inconsistency analysis, and invariant hunting
- **Solana protocol agent** (DEEP mode) — domain-specific checklists for lending, AMM/DEX, vaults, staking, bridges, governance, proxies, and session keys
- **False-positive gate** — every finding must pass 3 checks (concrete path, reachable entry point, no existing guard)
- **Confidence scoring** — base 100 with deductions for privileged callers, partial paths, self-contained impact, token assumptions, and external preconditions
- **Framework-aware** — works with Anchor, native Rust, and Pinocchio programs

## Usage

```bash
# Scan the full repo (default — 4 agents)
/solana-auditor

# Full repo + adversarial reasoning + protocol analysis (6 agents)
/solana-auditor deep

# Review specific file(s)
/solana-auditor programs/vault/src/lib.rs
/solana-auditor programs/vault/src/instructions/deposit.rs programs/vault/src/instructions/withdraw.rs

# Write report to a markdown file (terminal-only by default)
/solana-auditor --file-output
```

## Known Limitations

**Codebase size.** Works best up to ~2,500 lines of Rust. Past ~5,000 lines, triage accuracy and mid-bundle recall drop noticeably. For large codebases, run per program rather than everything at once.

**What AI misses.** AI is strong at pattern matching — missing account validations, unchecked arithmetic, known CPI pitfalls. It struggles with relational reasoning: multi-transaction state setups, specification/invariant bugs, cross-protocol composability, game-theory attacks, and off-chain assumptions. AI catches what humans forget to check. Humans catch what AI cannot reason about. You need both.

## 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), DEEP (+ adversarial reasoning + protocol analysis), or a specific filename.
---

# Solana Program Security Audit

You are the orchestrator of a parallelized Solana smart contract security audit. Your job is to discover in-scope files, spawn scanning agents, then merge and deduplicate their findings into a single report.

## Mode Selection

**Exclude pattern** (applies to all modes): 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) to discover files.
- **deep**: same scope as default, but also spawns the adversarial reasoning agent (Agent 5) and the Solana protocol analysis agent (Agent 6). Use for thorough reviews. Slower and more costly.
- **`$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`). Without this flag, output goes to the terminal only. Never write a report file unless the user explicitly passes `--file-output`.

## Version Check

After printing the banner, run two parallel tool calls: (a) Read the local `VERSION` file from the same directory as this skill, (b) Bash `curl -sf https://raw.githubusercontent.com/sanbir/solana-auditor-skills/main/solana-auditor/VERSION`. If the remote fetch succeeds and the versions differ, print:

> ⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/sanbir/solana-auditor-skills#install--run

Then continue normally. If the fetch fails (offline, timeout), skip silently.

## Orchestration

**Turn 1 — Discover.** Print the banner, then in the same message make parallel tool calls: (a) Bash `find` for in-scope `.rs` files per mode selection, (b) Glob for `**/references/attack-vectors/attack-vectors-1.md` and extract the `references/` directory path (two levels up). Use this resolved path as `{resolved_path}` for all subsequent references.

**Turn 2 — Prepare.** In a single message, make three parallel tool calls: (a) Read `{resolved_path}/agents/vector-scan-agent.md`, (b) Read `{resolved_path}/report-formatting.md`, (c) Bash: create four per-agent bundle files (`/tmp/audit-agent-{1,2,3,4}-bundle.md`) in a **single command** — each concatenates **all** in-scope `.rs` files (with `### path` headers and fenced code blocks), then `{resolved_path}/judging.md`, then `{resolved_path}/report-formatting.md`, then `{resolved_path}/attack-vectors/attack-vectors-N.md`; print line counts. Every agent receives the full codebase — only the attack-vectors file differs per agent. Do NOT read or inline any file content into agent prompts — the bundle files replace that entirely.

**Turn 3 — Spawn.** In a single message, spawn all agents as parallel foreground Agent tool calls (do NOT use `run_in_background`). Always spawn Agents 1–4. Only spawn Agents 5 and 6 when the mode is **DEEP**.

- **Agents 1–4** (vector scanning) — spawn with `model: "sonnet"`. Each agent prompt must contain the full text of `vector-scan-agent.md` (read in Turn 2, paste into every prompt). After the instructions, add: `Your bundle file is /tmp/audit-agent-N-bundle.md (XXXX lines).` (substitute the real line count).
- **Agent 5** (adversarial reasoning, DEEP only) — spawn with `model: "opus"`. Receives the in-scope `.rs` file paths and the instruction: your reference directory is `{resolved_path}`. Read `{resolved_path}/agents/adversarial-reasoning-agent.md` for your full instructions.
- **Agent 6** (Solana protocol analysis, DEEP only) — spawn with `model: "opus"`. Receives the in-scope `.rs` file paths and the instruction: your reference directory is `{resolved_path}`. Read `{resolved_path}/agents/solana-protocol-agent.md` for your full instructions.

**Turn 4 — Report.** Merge all agent results: deduplicate by root cause (keep the higher-confidence version), sort by confidence highest-first, re-number sequentially, and insert the **Below Confidence Threshold** separator row. Print findings directly — do not re-draft or re-describe them. Use report-formatting.md (read in Turn 2) for the scope table and output structure. If `--file-output` is set, write the report to a file (path per report-formatting.md) and print the path.

## Banner

Before doing anything else, print this exactly:

```

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

```

## VERSION

```

```

## assets

```

```

## assets/docs

```

```

## assets/docs/README.md

# Project Docs

Drop project-specific context here: specifications, invariants, design docs, known limitations. The audit agents will read these for additional context.

## assets/findings

```

```

## assets/findings/README.md

# Findings

Previous audit reports and findings are stored here. On subsequent runs, the skill can re-verify whether reported issues have been fixed.

## references

```

```

## references/agents

```

```

## references/agents/adversarial-reasoning-agent.md

# Adversarial Reasoning Agent Instructions

You are an adversarial security researcher trying to exploit these Solana programs. There are bugs here — find them. Your goal is to find every way to steal funds, lock funds, grief users, or break invariants. Do not give up. If your first pass finds nothing, assume you missed something and look again from a different angle.

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

## Reasoning Strategies

Use these three complementary approaches:

### 1. Feynman Questioning
For each instruction handler, ask: "What would happen if I called this with the most adversarial possible inputs?" Consider:
- Every account passed is attacker-controlled or spoofed
- Every instruction argument is at boundary values (0, 1, u64::MAX)
- Transaction ordering is adversarial (front-running, sandwich)
- Multiple instructions composed in one transaction

### 2. State Inconsistency Analysis
For every pair of instructions that share state:
- Can Instruction A leave state in a condition Instruction B doesn't expect?
- Can partial execution (A succeeds, B fails) create exploitable state?
- Can concurrent or reordered execution break invariants?
- Does a CPI in A modify state that B reads without reload?

### 3. Invariant Hunting
Identify implicit invariants the program assumes:
- **Conservation laws:** total_staked == sum(individual_stakes), total_supply == sum(balances)
- **Authority invariants:** only the stored authority can modify this account
- **Ordering invariants:** initialize must happen before deposit, deposit before withdraw
- **Economic invariants:** no operation should create tokens from nothing, no round-trip should be profitable

For each invariant, find instructions that could violate it.

## Solana-Specific Focus Areas

- **Account validation gaps:** missing ownership, signer, discriminator, or writable checks
- **PDA security:** non-canonical bumps, seed collisions, shared PDAs, missing user-specific seeds
- **CPI trust boundaries:** arbitrary CPI, signer escalation, stale data, unchecked returns
- **Token program edge cases:** Token-2022 extensions (hooks, fees, interest), legacy/Token-2022 confusion
- **Arithmetic:** unchecked overflow, precision loss, rounding direction, division-before-multiplication
- **Economic exploits:** first-depositor inflation, flash stake/unstake, fee bypass, dust DoS
- **State lifecycle:** reinitialization, revival attacks, improper closing, zombie accounts

## 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. Do not use any attack vector reference files — reason freely instead.
2. Reason freely about the code — apply the three strategies above. For each potential finding, apply the FP gate from `judging.md` immediately (three checks). If any check fails → drop and move on without elaborating. Only if all three pass → trace the full attack path, apply score deductions, and format the finding.
3. Your final response message MUST contain every finding **already formatted per `report-formatting.md`** — indicator + bold numbered title, location · confidence line, **Description** with one-sentence explanation, and **Fix** with diff block (omit fix for findings below 75 confidence). Use placeholder sequential numbers (the main agent will re-number).
4. Do not output findings during analysis — compile them all and return them together as your final response.
5. If you find NO findings, respond with "No findings."

## references/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/agents/vector-scan-agent.md

# Vector Scan Agent Instructions

You are a security auditor scanning Solana/Rust programs for vulnerabilities. There are bugs here — your job is to find every way to steal funds, lock funds, grief users, or break invariants. Do not accept "no findings" easily.

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

## Solana-Specific Context

- **Account model:** Every account is passed explicitly. Ownership, signer status, writable flags, and PDA derivation must all be validated — nothing is implicit.
- **Frameworks:** Code may use Anchor (macros, constraints, `Account<'info, T>`), native Rust (`next_account_info`, `invoke`), or Pinocchio (zero-copy). Adapt your analysis to the framework used.
- **CPI:** Cross-program invocations are trust boundaries. Check program ID validation, signer pass-through, stale data after CPI, and return value propagation.
- **PDAs:** Verify canonical bumps, user-specific seeds, type prefixes, and seed collision resistance.
- **Tokens:** Distinguish SPL Token (legacy) from Token-2022. Check for `transfer_checked`, transfer hooks, interest-bearing and transfer-fee extensions.

## Workflow

1. Read your bundle file in **parallel 1000-line chunks** on your first turn. The line count is in your prompt — compute the offsets and issue all Read calls at once (e.g., for a 5000-line file: `Read(file, limit=1000)`, `Read(file, offset=1000, limit=1000)`, `Read(file, offset=2000, limit=1000)`, `Read(file, offset=3000, limit=1000)`, `Read(file, offset=4000, limit=1000)`). Do NOT read without a limit. These are your ONLY file reads — do NOT read any other file after this step.
2. **Triage pass.** For each vector, classify into three tiers:
   - **Skip** — the named construct AND underlying concept are both absent (e.g., oracle vectors when no price feeds are used).
   - **Borderline** — the named construct is absent but the underlying vulnerability concept could manifest through a different mechanism (e.g., "stale oracle data" when the code caches any external state; "PDA sharing" when seeds lack user-specific components).
   - **Survive** — the construct or pattern is clearly present.
   Output all three tiers — every vector must appear in exactly one: `Skip: V1, V2 ...`, `Surviving: V3, V16 ...`, `Borderline: V8, V22 ...`. End with `Total: N classified` and verify it matches your vector count. Borderline vectors get a 1-sentence relevance check: only promote if you can (a) name the specific function/instruction where the concept manifests AND (b) describe in one sentence how the exploit would work; otherwise drop.
3. **Deep pass.** Only for surviving vectors. Use this **structured one-liner format** for each vector's analysis — do NOT write free-form paragraphs:
   ```
   V15: path: deposit() → vault PDA seeds lack user key → shared vault | guard: none | verdict: CONFIRM [85]
   V22: path: withdraw() → transfer CPI → reload() called after | guard: reload present | verdict: DROP (FP gate 3: guarded)
   ```
   For each vector: trace the call chain from the instruction handler to the vulnerable line — check every Anchor constraint, manual validation, modifier, and state guard. Consider alternate manifestations, not just the literal construct named. If no match or FP conditions fully apply → DROP in one line (never reconsider). If match → apply the FP gate from `judging.md` (three checks). If any check fails → DROP in one line. Only if all three pass → write CONFIRM with score deductions, then expand into the formatted finding below. **Budget: ≤1 line per dropped vector, ≤3 lines per confirmed vector before its formatted finding.**
4. **Composability check.** Only if you have 2+ confirmed findings: do any two compound (e.g., missing signer + duplicate accounts = unauthorized drain)? If so, note the interaction in the higher-confidence finding's description.
5. Your final response message MUST contain every finding **already formatted per `report-formatting.md`** — indicator + bold numbered title, location · confidence line, **Description** with one-sentence explanation, and **Fix** with diff block (omit fix for findings below 75 confidence). Use placeholder sequential numbers (the main agent will re-number).
6. Do not output findings during analysis — compile them all and return them together as your final response.
7. **Hard stop.** After the deep pass, STOP — do not re-examine eliminated vectors, scan outside your assigned vector set, or "revisit"/"reconsider" anything. Output your formatted findings, or "No findings." if none survive.

## references/attack-vectors

```

```

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

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

> Part 1 of 4 · Vectors 1–30 of 120 total
> Covers: signer checks, ownership, discriminators, account constraints, data matching, writable flags, reinitialization

Each vector follows the format:
- **D:** Description — what makes it exploitable
- **FP:** False-positive conditions — mitigations that would make it NOT a finding

---

**1. Missing Signer Check**

- **D:** Authority account used in a privileged instruction (withdraw, transfer, admin update) without verifying `is_signer`. Any account address can be passed without a signature, allowing unauthorized execution.
- **FP:** Anchor `Signer<'info>` type used. Native: explicit `if !account.is_signer` check present. Pinocchio: `is_signer()` validated.

**2. Missing Owner Check on Deserialized Account**

- **D:** Account data deserialized via `try_from_slice` or manual parsing without first checking `account.owner == expected_program_id`. Attacker crafts a fake account with identical data layout owned by a malicious program, spoofing balances or authorities.
- **FP:** Anchor `Account<'info, T>` used (automatic owner check). Native: explicit owner comparison before deserialization. Pinocchio: `is_owned_by()` check present.

**3. Type Cosplay — Missing Discriminator Check**

- **D:** Account struct deserialized without verifying its 8-byte discriminator. Attacker passes an `Admin` account where a `User` account is expected (or vice versa), bypassing privilege checks because the data layouts partially overlap.
- **FP:** Anchor `Account<'info, T>` or `#[account]` macro used (automatic discriminator). Native: manual discriminator byte check at offset 0. Zero-copy: `AccountLoader<'info, T>` with discriminator validation.

**4. Reinitialization Attack**

- **D:** Initialization instruction can be called on an already-initialized account, overwriting the authority field. Attacker reinitializes to become the new owner and drains controlled assets.
- **FP:** Anchor `init` constraint used (prevents reinit by checking discriminator + owner). Native: explicit `is_initialized` flag checked before setup logic. `init_if_needed` used with additional validation of existing state.

**5. init_if_needed Without State Validation**

- **D:** `init_if_needed` constraint used without checking existing account state when the account already exists. Attacker pre-initializes the account with harmful state (e.g., wrong authority) before the legitimate user calls the instruction.
- **FP:** Code validates existing data fields (owner, authority) when account already exists. `init` used instead of `init_if_needed`. Frontrunning protection via PDA seeds that include the payer's key.

**6. Missing has_one Constraint — Data Mismatch**

- **D:** Account relationship not validated — e.g., `vault.authority != signer.key()` not enforced. Attacker passes a vault they control instead of the victim's vault, or passes a mismatched token account.
- **FP:** Anchor `has_one = authority` constraint present. Native: manual pubkey comparison between stored field and provided account. Constraint logic validates cross-account relationships.

**7. Missing Writable Check**

- **D:** Account modified without being marked as writable in the transaction. In older runtime versions, this could cause silent state corruption. Even in newer versions, missing writable annotation in Anchor's `#[account(mut)]` means the framework won't serialize changes back.
- **FP:** Anchor `#[account(mut)]` present on all modified accounts. Native: explicit `is_writable` check before mutation.

**8. UncheckedAccount Without Manual Validation**

- **D:** `UncheckedAccount<'info>` or raw `AccountInfo<'info>` used without any manual ownership, signer, or discriminator checks. This is the most permissive account type — attacker can pass any account.
- **FP:** Manual checks present after `/// CHECK:` comment (owner, signer, key comparison, data validation). Account only used for reading lamport balance or key comparison (no deserialization).

**9. remaining_accounts Without Validation**

- **D:** `ctx.remaining_accounts` iterated without ownership, signer, type, or data checks. These accounts bypass Anchor's compile-time constraint system entirely — easiest injection point for malicious accounts.
- **FP:** Full validation loop present: owner check, discriminator check, and signer/writable checks applied to each remaining account. Non-zero data length check present.

**10. Missing Account Close Constraint — Data Accessible After Close**

- **D:** Account closed by only zeroing lamports without using Anchor's `close` constraint. Data remains readable within the same transaction. Attacker reads sensitive data or reuses the "closed" account in subsequent instructions.
- **FP:** Anchor `close = recipient` constraint used. Native: full close sequence (zero data → drain lamports → assign to System Program).

**11. Account Revival Attack**

- **D:** Account closed by draining lamports but not reassigning ownership to the System Program. Attacker refunds rent to the account address, "reviving" it with stale or manipulated data within the same transaction.
- **FP:** Proper close: data zeroed, lamports drained, owner set to System Program. Anchor `close` constraint used. Lamport check (`> 0`) before processing accounts.

**12. Operations on Closed Accounts**

- **D:** Instruction reads or writes to an account that was closed in a prior instruction within the same transaction. Account data is still accessible until the transaction completes, leading to inconsistent state.
- **FP:** Lamport balance checked (`> 0`) before operating on the account. Discriminator checked (closed accounts have zeroed discriminator). Transaction design prevents close + use in same tx.

**13. Duplicate Mutable Accounts**

- **D:** Same account passed for two different mutable parameters (e.g., `from` and `to` in a transfer). The last serialized write wins, effectively enabling free "transfers" to self that bypass balance checks or double state mutations.
- **FP:** Explicit constraint: `from.key() != to.key()`. Anchor constraint with `@ ErrorCode::SameAccount`. Single-reference pattern used when updating different fields of the same account.

**14. Missing Rent Exemption Check**

- **D:** New account funded with insufficient lamports — below the rent-exempt threshold. Account becomes eligible for garbage collection by the runtime, causing permanent data loss.
- **FP:** Anchor `init` with `payer` handles rent automatically. Native: `Rent::get()?.minimum_balance(data_len)` checked. System Program `create_account` called with correct lamports.

**15. Unintended Account Closure via close Constraint**

- **D:** `close` constraint applied to an account that should persist, or close destination set to an attacker-controllable address. Attacker triggers the close path and receives the rent lamports.
- **FP:** Close constraint only on accounts explicitly designed to be closeable. Close destination is a trusted address (original payer, program-controlled PDA). Access control on the close instruction.

**16. Missing Token Account Mint Validation**

- **D:** Token account used in a transfer without validating its `mint` field matches the expected mint. Attacker passes a token account for a different (worthless) mint, receiving valuable tokens in return.
- **FP:** Anchor `#[account(token::mint = expected_mint)]` constraint. Native: manual `token_account.mint == expected_mint` comparison. `has_one = mint` on the state account.

**17. Missing Token Account Authority Validation**

- **D:** Token account's `owner` (authority) field not validated against the expected authority. Attacker passes their own token account where the program expects a protocol-controlled account, redirecting funds.
- **FP:** Anchor `#[account(token::authority = expected_authority)]` constraint. `has_one = authority` on the vault state. Manual pubkey comparison of token account owner field.

**18. Sysvar Account Spoofing**

- **D:** Sysvar account (Clock, Rent, Instructions, SlotHashes) passed by user without verifying its public key matches the known sysvar address. Attacker passes a fake account with manipulated timestamps, rent values, or instruction data. (Wormhole exploit vector.)
- **FP:** Sysvar accessed via `Clock::get()?` or `Rent::get()?` (syscall, not account). Anchor `Sysvar<'info, Clock>` type used. Manual address comparison: `account.key == &sysvar::clock::ID`.

**19. Instruction Introspection with Absolute Index**

- **D:** `load_instruction_at(0, ...)` or `load_instruction_at_checked(N, ...)` used with an absolute index. Attacker crafts a transaction where the same instruction at index 0 is used to validate multiple program calls, bypassing intended one-time checks.
- **FP:** Relative indexing used: `get_instruction_relative(-1, ...)` or `load_current_index_checked()` + offset. Correlation validation between current and referenced instructions (same program ID, same accounts).

**20. Unchecked load_instruction_at (Pre-1.8.1)**

- **D:** `load_instruction_at()` (unchecked version) used instead of `load_instruction_at_checked()`. On Solana < 1.8.1, the sysvar account is not validated, allowing complete instruction spoofing.
- **FP:** `load_instruction_at_checked()` used. Solana runtime >= 1.8.1. Manual sysvar address validation before call.

**21. Missing Account Data Length Check**

- **D:** Account data deserialized without checking `account.data_len()` matches the expected struct size. Truncated or oversized data causes deserialization errors or reads garbage bytes.
- **FP:** Anchor handles this automatically via `Account<'info, T>`. Native: explicit `if account.data_len() != expected_size` check. Borsh deserialization with proper error handling.

**22. Account Confusion — System Program as Token Program**

- **D:** System Program account passed where Token Program is expected (or vice versa). Without program ID validation, the CPI call either fails silently or executes unexpected logic.
- **FP:** Anchor `Program<'info, Token>` or `Program<'info, System>` types used. Explicit `require_keys_eq!` against known program IDs.

**23. Missing Writable Requirement on PDA Signer**

- **D:** PDA used as a signer in CPI via `invoke_signed` but the target instruction expects the PDA account to be writable (e.g., for lamport transfer). Missing writable flag causes silent CPI failure.
- **FP:** PDA account marked as writable in both the `AccountMeta` and the Anchor `#[account(mut)]`. Transaction-level writable flag set correctly.

**24. Authority Transfer Without Timelock**

- **D:** Admin/authority can be transferred in a single instruction with no timelock, two-step process, or governance approval. A compromised key immediately takes full control of the protocol.
- **FP:** Two-step transfer: `propose_authority` + `accept_authority`. Timelock or governance vote required. Multi-sig authority.

**25. Missing Constraint on Config Account Update**

- **D:** Protocol config account (fee recipient, fee rate, pause flag) updated without sufficient access control or range validation. Attacker or compromised admin redirects fees to attacker-controlled address or sets fees to 100%.
- **FP:** Access control on config update instruction. Range validation on numeric parameters (`fee_bps <= MAX_FEE`). Fee recipient validated as a known protocol address.

**26. Frontrunnable Account Initialization**

- **D:** Account initialization uses predictable PDA seeds without including the payer's key. Attacker frontruns the legitimate initialization with their own authority, gaining control of the account.
- **FP:** PDA seeds include `payer.key()` or `authority.key()`. `init` constraint ensures single initialization. Seeds include unpredictable components.

**27. Missing is_initialized Flag Check (Native)**

- **D:** In native Rust programs without Anchor, no `is_initialized` boolean flag checked before processing an account. Attacker passes an uninitialized (zeroed) account, causing default/zero values to be treated as valid state.
- **FP:** Discriminator check (non-zero first bytes). Explicit `is_initialized` flag checked. Account owned by System Program rejected (uninitialized accounts are owned by System Program).

**28. Unconstrained Mint Authority**

- **D:** Mint authority not validated during token operations. Attacker mints arbitrary tokens if they can invoke the mint instruction without proper authority checks, inflating supply.
- **FP:** `mint::authority = expected_authority` constraint. Native: mint authority field compared to signer. CPI to token program includes authority validation.

**29. Unconstrained Freeze Authority**

- **D:** Token mint created with a freeze authority that is not set to `None` or a trusted address. Holder of freeze authority can freeze any token account at will, griefing users.
- **FP:** Freeze authority set to `None` at mint initialization. Freeze authority is a governance-controlled address. Token design explicitly requires freeze capability.

**30. Missing Mint Close Authority Validation**

- **D:** Mint close authority not set to `None` during initialization. A mint with close authority can be closed and re-initialized at the same address with different decimals, breaking all downstream accounting.
- **FP:** `mint_close_authority` asserted as `None` during init. Mint close authority is a protocol-controlled PDA. Token-2022 mint authority extensions explicitly managed.

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

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

> Part 2 of 4 · Vectors 31–60 of 120 total
> Covers: PDA derivation, seed security, CPI safety, invoke_signed, signer escalation, program validation, stale data

Each vector follows the format:
- **D:** Description — what makes it exploitable
- **FP:** False-positive conditions — mitigations that would make it NOT a finding

---

**31. Non-Canonical Bump Seed**

- **D:** PDA created or verified using `create_program_address` with a user-supplied bump instead of the canonical bump from `find_program_address`. Multiple valid bumps exist for the same seeds — attacker can derive alternate PDAs, fragmenting state or bypassing intended PDA checks.
- **FP:** Anchor `seeds` + `bump` constraint used (automatic canonical bump). Native: `find_program_address` used for derivation. Canonical bump stored in account data and reused.

**32. PDA Sharing — Missing User-Specific Seed**

- **D:** PDA seeds lack a user-specific component (e.g., `user.key()`). All users share the same PDA, meaning one user's action can affect another user's state. Classic example: a global vault PDA instead of per-user vaults.
- **FP:** Seeds include `user.key().as_ref()` or equivalent unique identifier. Design explicitly requires a shared/global account (e.g., protocol config). Access control prevents unauthorized state changes on shared accounts.

**33. Seed Concatenation Collision**

- **D:** PDA seeds constructed from variable-length user inputs without delimiters or fixed-length encoding. Seeds `["AB", "C"]` and `["A", "BC"]` produce the same PDA — attacker finds a collision to access another user's account.
- **FP:** All seeds are fixed-length (pubkeys, u8 arrays). Variable-length seeds use canonical delimiters or length prefixes. Seeds are hashed before use.

**34. Seed Collision Across Account Types**

- **D:** Different account types (vault, escrow, config) use seeds with no unique type prefix. An attacker's "vault" PDA collides with a legitimate "escrow" PDA, enabling cross-type access.
- **FP:** Unique string prefixes per type: `b"vault"`, `b"escrow"`, `b"config"`. Seeds include account type discriminator.

**35. PDA Purpose Isolation Violation**

- **D:** Single PDA used across multiple logical domains or external programs. If one domain is compromised, the shared PDA grants access to all domains.
- **FP:** Each distinct capability (vault, escrow, staking) uses a distinct PDA with distinct seeds. Program-specific seeds prevent cross-program PDA reuse.

**36. Exposed PDA Seeds — User-Controllable Derivation**

- **D:** PDA seeds entirely composed of user-controlled inputs without any program-controlled components. Attacker can pre-compute and pre-initialize PDAs to front-run legitimate users.
- **FP:** Seeds include program-controlled values (authority pubkey, protocol nonce). `init` constraint prevents re-initialization. Seeds include the payer's key.

**37. Forced Seed De-Bump**

- **D:** Program accepts a bump seed from the user and doesn't verify it's canonical. Attacker provides bump = 0 or a non-canonical bump, causing `create_program_address` to fail or derive a different address than expected.
- **FP:** Bump stored on-chain and reused. Anchor `bump` constraint validates canonical bump automatically. `find_program_address` used to derive and verify.

**38. Arbitrary CPI — Unvalidated Program ID**

- **D:** `invoke()` or `invoke_signed()` called with a program ID from an unvalidated `AccountInfo`. Attacker passes a malicious program that mimics the expected interface — returns success without performing the operation, or performs a different operation (e.g., reverse transfer).
- **FP:** Anchor `Program<'info, Token>` type used (automatic validation). Native: `require_keys_eq!(program.key(), expected_program::ID)`. Program ID hardcoded in CPI call.

**39. CPI Without Signer Seeds — invoke vs invoke_signed Confusion**

- **D:** `invoke()` used where `invoke_signed()` is required because a PDA needs to sign the CPI. The CPI fails silently or panics. Conversely, `invoke_signed()` used unnecessarily, potentially escalating signer privileges.
- **FP:** PDA signer correctly identified — `invoke_signed` used with proper seeds. Non-PDA CPI correctly uses `invoke`. Anchor CPI context correctly constructed.

**40. Signer Pass-Through in CPI**

- **D:** All accounts from the current instruction passed into a CPI call without filtering. Signer accounts retain their signer privilege in the CPI — a malicious callee can use the signer authority to perform unauthorized actions.
- **FP:** Only necessary accounts passed to CPI. Signer accounts explicitly filtered out unless required. Account isolation via user-specific PDAs limits blast radius.

**41. SOL Balance Drain via CPI**

- **D:** Signer account passed to an external CPI. The callee program can spend SOL from the signer (Solana has no `msg.value` equivalent — any signer can be drained). No balance check before/after CPI.
- **FP:** `signer.lamports()` recorded before CPI, verified after: `balance_before <= balance_after + max_spendable`. CPI target is a trusted, verified program. PDA used instead of user signer for CPI authority.

**42. Post-CPI Ownership Change**

- **D:** An attacker-controlled program uses the `assign` instruction during CPI to change an account's owner. After the CPI returns, the account is no longer owned by the expected program, but the caller doesn't re-verify.
- **FP:** Account owner re-checked after CPI: `account.owner == expected_program`. CPI target is a trusted program (SPL Token, System Program). Account is a PDA owned by the calling program.

**43. Stale Data After CPI — Missing reload()**

- **D:** Account data deserialized before a CPI, then used after the CPI without calling `reload()`. The CPI may have modified the on-chain state, but the in-memory struct still holds the pre-CPI values. Decisions based on stale balances enable double-spends or over-withdrawals.
- **FP:** `ctx.accounts.account.reload()?` called after every CPI that modifies shared accounts. Account only read after all CPIs complete. No CPI modifies the account in question.

**44. CPI Return Value Ignored**

- **D:** CPI invocation result not propagated with `?`. If the inner call fails, the outer instruction continues executing with an inconsistent state (e.g., transfer failed but balance decremented).
- **FP:** All CPI calls wrapped with `?` operator. Anchor CPI helpers (`token::transfer(ctx, amount)?`) used. Native: `invoke(...)?.` or explicit match on result.

**45. Unnecessary Accounts Passed to CPI**

- **D:** More accounts than needed passed to a CPI call. The callee gains read/write access to accounts it shouldn't touch, expanding the attack surface.
- **FP:** CPI account lists contain only the minimum required accounts. Anchor CPI context structs enforce exact account requirements.

**46. invoke_signed with Incorrect Seeds**

- **D:** `invoke_signed` called with wrong seeds or wrong bump, causing PDA signature to fail. In some cases, the wrong seeds derive a valid but unintended PDA, causing the wrong account to sign.
- **FP:** Seeds match the PDA derivation exactly. Stored canonical bump used. Anchor `CpiContext::new_with_signer` with correct signer seeds.

**47. CPI to System Program — Unintended Account Creation**

- **D:** CPI to System Program's `create_account` or `transfer` without verifying the destination account. Attacker forces creation of accounts at unexpected addresses or redirects SOL transfers.
- **FP:** Destination account validated as expected PDA or known address. Account creation uses PDA seeds (deterministic). Transfer destination is a protocol-controlled account.

**48. Missing Program ID Check on Token Program**

- **D:** Token operations (transfer, mint, burn) performed via CPI without distinguishing between SPL Token (legacy) and Token-2022. Using the wrong program ID causes silent failures or fund loss when Token-2022 mints are involved.
- **FP:** Anchor `Interface<'info, TokenInterface>` or `InterfaceAccount` used. Native: dynamic program ID detection based on mint owner. `transfer_checked` used with correct program.

**49. Token-2022 Incompatibility — Legacy transfer Used**

- **D:** `anchor_spl::token::transfer` (or native equivalent) hardcodes the legacy Token Program ID. When used with Token-2022 mints, the CPI fails or misbehaves, causing DoS or fund loss.
- **FP:** `transfer_checked` used for all token operations. `InterfaceAccount` types detect correct program. Mint and decimals provided in transfer (required by `transfer_checked`).

**50. Token-2022 Transfer Hook Not Accounted For**

- **D:** Token-2022 mint has a transfer hook extension, but the program doesn't pass the required extra accounts for the hook CPI. Transfer silently fails or reverts.
- **FP:** Transfer hook accounts resolved and passed via `remaining_accounts`. Program checks for transfer hook extension on the mint. Only legacy tokens supported (documented and enforced).

**51. CPI Privilege Escalation via invoke_signed**

- **D:** `invoke_signed` extends signer privileges to accounts that shouldn't be signers. Attacker exploits the elevated privilege to authorize operations on accounts they don't control.
- **FP:** Only PDA accounts given signer privilege via `invoke_signed`. Non-PDA accounts explicitly not included in signer seeds. Minimum necessary privileges granted.

**52. Missing CPI Program ID Validation in Anchor**

- **D:** Anchor instruction uses `/// CHECK:` on a program account instead of `Program<'info, T>`. The program ID is never validated, enabling arbitrary CPI.
- **FP:** `Program<'info, Token>` or equivalent typed program account used. Manual `require_keys_eq!` check on program key. `address` constraint on the program account.

**53. Cross-Program Reentrancy via CPI Callback**

- **D:** Program makes a CPI to an external program that calls back into the original program before the first call completes. State is partially updated — the callback sees inconsistent state and can exploit it.
- **FP:** State fully updated before any CPI (checks-effects-interactions pattern). Reentrancy guard flag set before CPI, checked on entry. No external CPI to untrusted programs.

**54. PDA Bump Not Stored — Recomputation Cost**

- **D:** Canonical bump not stored in account data, forcing `find_program_address` on every instruction. This wastes ~2000 CU per call. While not a security vulnerability itself, it can push complex transactions over the compute budget, causing DoS.
- **FP:** Bump stored as `pub bump: u8` in account struct. `create_program_address` used with stored bump for re-derivation. Compute budget adequate for the operation.

**55. PDA Used as Signer Without Ownership Verification**

- **D:** PDA derived from user-controlled seeds used as a signer, but the program doesn't verify it owns the PDA. Attacker derives a PDA that belongs to their malicious program and uses it to sign unauthorized CPIs.
- **FP:** PDA verified as owned by the current program before use as signer. Seeds include program-specific constants. Anchor `seeds` constraint verifies PDA derivation.

**56. CPI to Upgradeable Program Without Freeze Check**

- **D:** CPI target is an upgradeable program that could be maliciously upgraded between the time the call is validated and executed. An upgrade changes the program's behavior, potentially converting a safe CPI into a malicious one.
- **FP:** CPI target is a non-upgradeable (frozen) program. Program upgrade authority validated as trusted. Immutable programs (SPL Token, System Program) used.

**57. Address Lookup Table Contains Signer**

- **D:** Signer pubkey included in an Address Lookup Table (ALT). Signer pubkeys must always be inline in the transaction — inclusion in ALT breaks transaction signing validation.
- **FP:** Only non-signer accounts in ALT. Signer accounts always included directly in transaction. ALT not used.

**58. Durable Nonce Not First Instruction**

- **D:** `AdvanceNonceAccount` instruction placed after other instructions in a transaction using durable nonces. The nonce doesn't advance, potentially allowing transaction replay.
- **FP:** `AdvanceNonceAccount` is the first instruction. Durable nonces not used. Transaction uses recent blockhash instead.

**59. Cross-Program State Desync**

- **D:** Program reads state from an external program's account, caches it, then makes decisions based on the cached value. Between the read and the decision, another instruction modifies the external state. The program acts on stale cross-program data.
- **FP:** State read and decision in the same instruction with no intervening CPI. External state re-read after any CPI. Atomic transaction design ensures consistency.

**60. Unvalidated remaining_accounts Used in CPI**

- **D:** `remaining_accounts` passed directly into a CPI call without validation. Attacker injects malicious accounts into the remaining accounts list, which the CPI callee processes as legitimate.
- **FP:** Each remaining account validated (owner, key, type) before CPI. remaining_accounts not passed to CPI. CPI uses only named, validated accounts.

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

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

> Part 3 of 4 · Vectors 61–90 of 120 total
> Covers: integer safety, precision loss, token operations, state lifecycle, account closing, fee logic, dust attacks

Each vector follows the format:
- **D:** Description — what makes it exploitable
- **FP:** False-positive conditions — mitigations that would make it NOT a finding

---

**61. Integer Overflow via Unchecked Arithmetic**

- **D:** Standard `+`, `-`, `*` operators used on `u64`/`u128` financial values. In release mode, Rust wraps on overflow — a deposit of `u64::MAX - 100` plus `200` wraps to `99`, effectively destroying funds or creating tokens from nothing.
- **FP:** `.checked_add()`, `.checked_sub()`, `.checked_mul()`, `.checked_div()` used with `?` propagation. `overflow-checks = true` in Cargo.toml `[profile.release]`. Anchor's `require!` with checked math.

**62. Integer Underflow on Balance Subtraction**

- **D:** Balance subtraction without underflow check: `vault.balance -= amount` where `amount > balance`. Wraps to a massive positive value, crediting the attacker with near-unlimited funds.
- **FP:** `.checked_sub()` used. `require!(vault.balance >= amount)` check before subtraction. Anchor constraint validation.

**63. Division Before Multiplication — Precision Loss**

- **D:** `(amount / total_supply) * price` truncates the division result before multiplying, losing precision permanently. Attacker exploits by using amounts that truncate to zero, getting free operations.
- **FP:** Multiplication performed first: `(amount * price) / total_supply`. Higher-precision intermediate type (u128) used. Decimal/fixed-point library used for calculations.

**64. Division by Zero**

- **D:** Division operation where the divisor can be zero (e.g., `total_supply`, `pool_balance`, `shares_outstanding`). Causes a panic that aborts the transaction, enabling DoS.
- **FP:** Explicit zero check: `require!(divisor > 0)`. `.checked_div()` used. Early return or special case when divisor is zero.

**65. Unsafe Integer Casting — Type Narrowing**

- **D:** Wider type cast to narrower type without bounds check: `value as u32` where `value: u64`. Silently truncates, causing incorrect amounts in transfers, fees, or state updates.
- **FP:** Explicit bounds check: `require!(val <= u32::MAX as u64)`. `try_into()` with error handling used. Consistent types throughout (no narrowing needed).

**66. Rounding Direction Exploitation**

- **D:** Share/token calculations always round in the user's favor. On deposits, rounding up gives slightly more shares; on withdrawals, rounding up gives slightly more tokens. Repeated small operations slowly drain the pool.
- **FP:** Rounding direction favors the protocol: round down on deposits (fewer shares), round up on withdrawals (fewer tokens returned). `floor` for minting, `ceil` for burning.

**67. First Depositor Vault Inflation Attack**

- **D:** First depositor mints shares, then donates tokens directly to the vault, inflating the share price. Subsequent depositors' deposits are rounded down to zero shares due to the inflated price, and the first depositor steals their tokens.
- **FP:** Virtual offset: vault starts with non-zero virtual shares/assets. Minimum deposit enforced. Dead shares minted on initialization. `require!(shares > 0)` on deposit.

**68. Round-Trip Profit — Deposit/Withdraw Arbitrage**

- **D:** Due to inconsistent rounding between deposit and withdraw operations, a user can deposit and immediately withdraw for a net profit. Repeated round-trips drain the pool.
- **FP:** Rounding consistently favors the pool in both directions. Minimum lock period between deposit and withdraw. Round-trip test: `deposit(X) → withdraw(all) ≤ X`.

**69. Saturating Math Misuse**

- **D:** `.saturating_sub()` used where underflow should be an error, not silently clamped to zero. A health factor or balance clamped to zero instead of reverting enables invalid state transitions.
- **FP:** `saturating_sub` only used where a floor of zero is semantically correct (e.g., remaining time). `.checked_sub()` used for financial calculations. Explicit `require!` before subtraction.

**70. Price Slippage Not Enforced**

- **D:** Swap, purchase, or pricing function lacks a user-provided `min_amount_out` or `max_price` parameter. MEV bots sandwich the transaction, manipulating price between submission and execution.
- **FP:** `expected_price` or `min_amount_out` parameter required. Slippage tolerance enforced with `require!`. Deadline parameter prevents stale execution.

**71. Lamport Balance Invariant Violation**

- **D:** Custom logic creates or destroys lamports — violating Solana's invariant that total lamports across all accounts in an instruction must remain constant. Can cause runtime errors or exploitable accounting discrepancies.
- **FP:** All lamport transfers are balanced: sum of debits equals sum of credits. System Program used for lamport transfers. Manual lamport math verified with assertions.

**72. Rent Lamports Sent to Arbitrary Destination**

- **D:** When closing an account, rent-exempt lamports transferred to a user-specified destination without validation. Attacker redirects rent from protocol accounts to themselves.
- **FP:** Close destination hardcoded to original payer or program-controlled address. Anchor `close = known_recipient`. Validated recipient address.

**73. Token Dust Account Poisoning**

- **D:** Attacker deposits a tiny (dust) amount into a token account, preventing it from being closed (close requires zero balance). This permanently blocks account closure, leaking rent and preventing state cleanup.
- **FP:** Dust swept or burned before close. Dust threshold defined — operations below threshold rejected. `close_account` with force flag if balance is below dust threshold.

**74. Fee Bypass on Alternative Code Path**

- **D:** Protocol fee applied on one code path (e.g., normal withdrawal) but not on another (e.g., emergency withdrawal, single-asset exit). Attacker uses the fee-free path to avoid paying fees.
- **FP:** Fees applied on every exit/withdrawal path. Single fee calculation function used across all paths. Fee-free paths have access control (e.g., admin-only emergency).

**75. Pre-Fee / Post-Fee Amount Confusion**

- **D:** Fee calculated on the pre-fee amount but the capacity check uses the post-fee amount (or vice versa). This creates accounting mismatches — either overfilling beyond capacity or under-charging fees.
- **FP:** Consistent amount used: either pre-fee throughout or post-fee throughout. Fee deducted atomically with the principal operation. Clear naming: `amount_before_fee`, `amount_after_fee`.

**76. Fee Deduction Not Atomic with Transfer**

- **D:** Fee calculated and deducted in a separate instruction from the transfer. Attacker skips the fee instruction or reorders instructions to avoid payment.
- **FP:** Fee deducted in the same instruction as the transfer. Atomic transaction design. Fee deducted from the transferred amount (not a separate call).

**77. Token Decimals Mismatch**

- **D:** Token operations assume a specific decimal count (e.g., 6 for USDC) without reading the mint's actual `decimals` field. When a token with different decimals is used, amounts are off by orders of magnitude.
- **FP:** `mint.decimals` read and used in all calculations. `transfer_checked` used (requires decimals parameter). Decimal normalization applied.

**78. Missing Transfer Amount Validation**

- **D:** Transfer instruction accepts `amount = 0` without validation. Zero-amount transfers can be used to trigger side effects (events, state updates, reward snapshots) without actual economic commitment.
- **FP:** `require!(amount > 0)` check on all transfer/deposit/withdraw instructions. Minimum amount enforced. Zero-amount short-circuits to no-op.

**79. Coupled State Fields Not Reset Atomically**

- **D:** Account has logically coupled fields (e.g., `shares_pending` + `total_shares`, `rewards_owed` + `last_claim_time`). On close or completion, one field is reset but the other isn't, leaving the account in an inconsistent state exploitable in future operations.
- **FP:** All coupled fields reset in the same instruction. Struct method that resets all related fields atomically. Close constraint zeros entire account data.

**80. Counter Drift — Statistic Not Updated Atomically**

- **D:** Global counters (total_deposits, total_users, volume) updated in a separate step from the operation that triggers them. If the update is skipped (error, reorder), counters drift, breaking protocol invariants.
- **FP:** Counters updated in the same instruction as the triggering operation. Atomic increment: `counter = counter.checked_add(1)?`. Counter can be re-derived from on-chain state if needed.

**81. Time Unit Mismatch — Slots vs Seconds**

- **D:** One part of the code uses slot numbers, another uses Unix timestamps (seconds), but they're compared directly. A vesting window in seconds compared to slot-based timestamps can unlock 4× earlier than intended.
- **FP:** Single canonical time unit used throughout. Explicit scale factor applied when converting. Field names annotated: `_slot`, `_timestamp_secs`. `Clock::get()?.unix_timestamp` used consistently.

**82. Stale Clock — Using Cached Timestamp**

- **D:** `Clock::get()` called once, timestamp cached, then used across multiple operations within the instruction. For most programs this is fine, but if the instruction spans multiple CPI calls that depend on ordering, the cached value may not reflect the expected time context.
- **FP:** `Clock::get()` called once per instruction (normal and expected). Timestamp used only for comparison, not for absolute scheduling. No time-sensitive CPI interleaving.

**83. Account Data Realloc Without Zero-Init**

- **D:** Account data reallocated to a larger size without zero-initializing the new bytes. Old data from the memory allocator may leak into the new space, causing unpredictable behavior.
- **FP:** Anchor `realloc` with `zero` = true constraint. Native: `memset` on new bytes. New space explicitly initialized before use.

**84. Unbounded Collection — Compute DoS**

- **D:** Instruction iterates over a variable-length collection (vector, remaining_accounts, linked list) without an upper bound. Attacker grows the collection until iteration exceeds the compute budget, permanently DoS-ing the instruction.
- **FP:** Fixed upper bound on collection size. Pagination pattern used. `SetComputeUnitLimit` budgeted for worst case. Iteration short-circuits or processes in batches.

**85. Self-Transfer Inflates Fee Claims**

- **D:** Transfer function allows `source == destination`. A self-transfer that triggers fee calculation or reward snapshot allows the user to accumulate fees/rewards without actual economic activity.
- **FP:** `require!(source.key() != destination.key())` check. Self-transfer short-circuits to no-op. Fee calculation skipped for zero-net-movement operations.

**86. Missing Preprocessing on Share Transfer**

- **D:** Shares or LP tokens transferred between users without settling pending fees/rewards on both source and destination first. Destination receives fees they never earned; source loses fees they're owed.
- **FP:** Both source and destination preprocessed (fees settled, reward accumulators snapshotted) before share transfer. Automatic settlement via transfer hook. Fee settlement in same instruction.

**87. Expired Account Not Closeable**

- **D:** Time-limited accounts (offers, escrows, locks) have no expiry-based close mechanism. Expired accounts leak rent indefinitely and can be griefed by keeping them alive.
- **FP:** Anyone can close expired accounts (not just creator). Expiry check: `require!(clock.unix_timestamp >= expiry)`. Automatic cleanup via crank or keeper.

**88. Token-2022 Interest-Bearing Token Not Accounted For**

- **D:** Token-2022 interest-bearing extension modifies the effective balance over time, but the program reads the raw balance. Calculations based on raw balance are incorrect, leading to under/over-accounting.
- **FP:** Interest-bearing extension detected and effective balance calculated. Only standard SPL tokens supported (documented and enforced). Token-2022 extensions explicitly handled.

**89. Token-2022 Transfer Fee Not Accounted For**

- **D:** Token-2022 transfer fee extension deducts a fee on every transfer, but the program assumes the full amount arrives at the destination. Accounting becomes inconsistent — the program credits more than was received.
- **FP:** Transfer fee extension detected. Post-transfer balance checked instead of using input amount. `transfer_checked` with fee calculation. Only non-fee tokens supported (enforced).

**90. Unsafe Rust Block Without Justification**

- **D:** `unsafe` block used for performance optimization (zero-copy, raw pointer access) without proper bounds checking. Memory corruption or out-of-bounds read can lead to arbitrary state manipulation.
- **FP:** No `unsafe` blocks in the codebase. `unsafe` blocks have explicit safety comments and bounds checks. `bytemuck` or `zerocopy` used instead of raw pointer manipulation.

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

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

> Part 4 of 4 · Vectors 91–120 of 120 total
> Covers: oracle manipulation, DeFi protocol patterns, staking/rewards, compute budget, logging, input validation, protocol economics

Each vector follows the format:
- **D:** Description — what makes it exploitable
- **FP:** False-positive conditions — mitigations that would make it NOT a finding

---

**91. Stale Oracle Price**

- **D:** Oracle price feed used without checking the `publish_time` or `last_update` timestamp. Oracle may have stopped updating hours ago — attacker exploits stale price to buy/sell at favorable outdated rates.
- **FP:** Staleness check: `require!(clock.unix_timestamp - price.publish_time <= MAX_AGE_SECONDS)`. `MAX_AGE_SECONDS` is admin-configurable. Price rejected if older than threshold.

**92. Oracle Confidence Interval Not Validated**

- **D:** Oracle price used without checking the confidence interval. Wide confidence (high `conf / price` ratio) means the price is unreliable — acting on it enables price manipulation or unfavorable trades.
- **FP:** Confidence check: `require!(price.conf * 100 / price.price <= MAX_CONF_PCT)`. Threshold is configurable. Price rejected if confidence is too wide.

**93. Oracle Status Not Checked**

- **D:** Oracle account read without verifying the price status (e.g., `PriceStatus::Trading`). Non-trading status (halted, unknown) may contain stale or invalid prices.
- **FP:** Status check: `require!(price_feed.status == PriceStatus::Trading)`. Error returned for non-trading status.

**94. Fake Oracle Account — Missing Owner Validation**

- **D:** Oracle account deserialized without checking its owner matches the oracle program (e.g., Pyth, Switchboard). Attacker creates a fake oracle account with manipulated price data.
- **FP:** `require_keys_eq!(*oracle.owner, PYTH_PROGRAM_ID)`. Anchor `Account<'info, PriceAccount>` with owner constraint. Oracle account address hardcoded or stored in validated config.

**95. Retroactive Oracle Pricing**

- **D:** Current oracle price used to settle positions that were opened at a different price. Instead of storing the reference price at open time, the program uses the live price at settlement, enabling manipulation.
- **FP:** Reference price stored in position account at open time. Settlement uses stored reference price. Price updates only affect new positions.

**96. On-Chain Price as Slippage Reference**

- **D:** Slippage protection calculated using an on-chain price (oracle, pool spot price) instead of a user-provided expected price. Attacker manipulates the on-chain price via flash loan, then the "slippage check" uses the manipulated value.
- **FP:** Slippage parameter provided by user off-chain (`min_amount_out`, `max_price`). TWAP used instead of spot price. Flash-loan-resistant price source.

**97. Flash Loan Price Manipulation**

- **D:** Protocol uses spot pool reserves or AMM price for valuation. Attacker takes a flash loan, manipulates pool reserves to move the price, executes the vulnerable operation at the manipulated price, then repays.
- **FP:** TWAP or oracle price used instead of spot. Manipulation-resistant price source. Flash loan detection (same-slot check). Minimum holding period.

**98. Vault Share Inflation — First Depositor Attack**

- **D:** Empty vault/pool allows first depositor to mint 1 share, then donate directly to inflate share price. Second depositor's deposit truncates to 0 shares — first depositor redeems for both deposits.
- **FP:** Virtual shares/assets offset in vault math. Minimum first deposit enforced. Dead shares minted to burn address on init. `require!(minted_shares > 0)`.

**99. Staking Reward Index Not Updated Before Balance Change**

- **D:** Staking contract doesn't update `rewardPerToken` or equivalent accumulator before stake/unstake operations. New staker gets credit for rewards earned before they staked; unstaker loses pending rewards.
- **FP:** Reward accumulator updated before any balance change. `update_rewards()` called at start of stake/unstake. Checkpoint pattern implemented.

**100. Flash Stake/Unstake Reward Capture**

- **D:** No minimum staking duration — attacker flash-deposits before a reward distribution, captures the reward, and immediately withdraws. Gets rewards without any real staking commitment.
- **FP:** Minimum staking/lockup period enforced. Reward distribution pro-rated over time. Snapshot-based rewards from past block/slot.

**101. Reward Dilution via Direct Transfer**

- **D:** Reward calculation based on token balance (`token_account.amount`) rather than internal accounting. Attacker transfers tokens directly to the reward pool, diluting all stakers' reward rates or manipulating the reward-per-token ratio.
- **FP:** Internal accounting tracks deposits separately from raw balance. Rewards calculated from `total_staked` state variable, not balance. Direct transfers don't affect reward math.

**102. Precision Loss Zeroing Small Stakers**

- **D:** Reward calculation for small stakers rounds to zero due to integer division: `(small_stake * reward_rate) / total_stake = 0`. Small stakers permanently earn zero rewards while their stake still dilutes others.
- **FP:** High-precision accumulator (u128 or fixed-point) used. Minimum stake enforced above precision threshold. Accumulated reward tracking prevents rounding to zero.

**103. Cooldown/Unstake Period Griefable by Dust**

- **D:** Unstaking cooldown resets on any new deposit. Attacker sends dust deposits to victim's staking position, perpetually resetting their cooldown and locking their funds.
- **FP:** Cooldown tracks per-deposit or doesn't reset on new deposits. Only the staker themselves can modify their position. Dust deposits below threshold rejected.

**104. Liquidation Incentive Insufficient for Small Positions**

- **D:** Liquidation bonus (percentage-based) on small/dust positions doesn't cover the gas/transaction cost for liquidators. Positions become permanently unliquidatable, accumulating bad debt.
- **FP:** Minimum position size enforced. Fixed minimum liquidation bonus in addition to percentage. Dust position auto-liquidation by protocol.

**105. Self-Liquidation Profitable**

- **D:** User can liquidate their own position and profit from the liquidation bonus. The bonus exceeds the penalty, creating a risk-free arbitrage that drains protocol reserves.
- **FP:** Self-liquidation prohibited (`liquidator != borrower`). Liquidation bonus < penalty. Health factor check prevents liquidation of healthy positions.

**106. Interest Accrual During Protocol Pause**

- **D:** Protocol pauses operations (deposits, withdrawals) but interest continues accruing. When unpaused, users face unexpected interest charges or liquidation from interest accumulated during pause.
- **FP:** Interest accrual paused alongside operations. Accumulated interest during pause forgiven or capped. Pause doesn't affect user positions.

**107. Compute Budget Exhaustion DoS**

- **D:** Instruction requires more compute units than the default 200K (or even the maximum 1.4M) due to complex calculations, large iterations, or multiple CPIs. Transaction always fails, permanently DoS-ing the functionality.
- **FP:** `SetComputeUnitLimit` called with adequate budget. Operations batched to fit within compute limits. Iteration bounded. Complex math optimized.

**108. Unbounded Log Output — Silent Truncation**

- **D:** Program emits large log messages that exceed Solana's ~10KB per-transaction log limit. Logs are silently truncated, losing critical audit trail data. Not exploitable directly but masks attacks.
- **FP:** Log messages kept concise. Critical data emitted as structured events (fixed-size). State persisted on-chain, not only in logs.

**109. Vec Initialization Bug — Comma vs Semicolon**

- **D:** `vec![0, N]` (comma) used instead of `vec![0; N]` (semicolon). Creates a two-element vector `[0, N]` instead of N zeroes. Accessing index 2+ panics, causing DoS.
- **FP:** `vec![0; N]` (semicolon) used correctly. Fixed-size arrays used instead of Vec. No dynamic Vec initialization.

**110. Unconstrained Mint — Supply Inflation**

- **D:** Mint instruction callable without proper authority validation, allowing anyone to mint tokens. Inflates supply, devaluing all existing tokens.
- **FP:** Mint authority validated as signer. `mint::authority = expected` constraint. Supply cap enforced.

**111. Unconstrained Burn — Supply Deflation**

- **D:** Burn instruction callable on any user's tokens without their authorization. Attacker burns other users' tokens, causing permanent fund loss.
- **FP:** Token account owner must be signer for burn. `token::authority = signer` constraint. Only self-burn allowed.

**112. Missing Input Amount Validation**

- **D:** Instruction accepts amounts without bounds checking — amounts of 0, `u64::MAX`, or values outside protocol's operational range. Zero amounts trigger side effects without commitment; max amounts overflow calculations.
- **FP:** `require!(amount > 0 && amount <= MAX_AMOUNT)` on all user inputs. Minimum and maximum amounts enforced. Protocol-specific bounds validated.

**113. Unconstrained Fee Recipient Update**

- **D:** Fee recipient address updateable by admin without validation. Compromised admin redirects all protocol fees to attacker address.
- **FP:** Fee recipient update has timelock. New recipient validated against allowlist. Multi-sig required for updates.

**114. Protocol Config Allows Zero-Fee Path**

- **D:** Admin can set fee to 0%, creating a zero-fee path that drains protocol revenue or enables wash trading without cost.
- **FP:** Minimum fee enforced: `require!(fee_bps >= MIN_FEE_BPS)`. Fee changes require governance. Zero fee only in specific contexts (e.g., whitelisted addresses).

**115. Missing same-asset Check in Swap**

- **D:** Swap function accepts `input_mint == output_mint`. Same-token swap can be exploited to manipulate fee accounting or pool invariants without actual economic activity.
- **FP:** `require!(input_mint != output_mint)` check. Same-asset short-circuits to no-op. Pool invariant checked after swap.

**116. Unchecked Realloc — Account Data Overflow**

- **D:** Account data reallocated beyond the 10MB limit or without proper space calculation. Realloc to smaller size truncates data; realloc to larger size without paying rent causes runtime error.
- **FP:** Realloc size calculated correctly. Rent difference paid. Anchor `realloc` constraint with proper space calculation. Size bounds checked.

**117. Event Logging Inconsistency**

- **D:** Critical state-changing events not emitted or emitted with incorrect data. Off-chain indexers miss state changes, causing UI inconsistencies or delayed responses. Not directly exploitable but enables masked attacks.
- **FP:** Events emitted for all state changes. Event data matches actual state changes. Structured events with all relevant fields.

**118. Unchecked Return Data from CPI**

- **D:** CPI return data assumed to be in a specific format without validation. Malicious program returns unexpected data format, causing deserialization error or misinterpreted values.
- **FP:** CPI target is a trusted program. Return data parsed with proper error handling. `sol_get_return_data()` result validated.

**119. Missing Deadline on Time-Sensitive Operations**

- **D:** Swap, deposit, or other time-sensitive operation has no deadline parameter. Transaction sits in mempool, executes hours later at stale prices or unfavorable conditions.
- **FP:** `deadline` or `valid_until` parameter required. `require!(clock.unix_timestamp <= deadline)` check. Transaction recentness enforced.

**120. Program Upgrade Authority Not Secured**

- **D:** Program's upgrade authority is a single hot wallet. Compromised key can deploy malicious code, draining all protocol funds. This is the highest-impact vector for upgradeable programs.
- **FP:** Upgrade authority is a multi-sig or governance-controlled address. Program is immutable (upgrade authority set to `None`). Timelock on upgrades. Verifiable build deployed.

## references/judging.md

# Finding Validation

Each finding passes a false-positive gate, then gets a confidence score (how certain you are it is real).

## FP Gate

Every finding must pass all three checks. If any check fails, drop the finding — do not score or report it.

1. You can trace a concrete attack path: caller → instruction handler → state change → loss/impact. Evaluate what the code _allows_, not what the deployer _might choose_.
2. The entry point is reachable by the attacker (check Anchor constraints, `Signer` types, `has_one`, access control, PDA-only instructions).
3. No existing guard already prevents the attack (Anchor constraints, `require!`, manual checks, `if`-revert, reentrancy flags, etc.).

## Confidence Score

Confidence measures certainty that the finding is real and exploitable — not how severe it is. Every finding that passes the FP gate starts at **100**.

**Deductions (apply all that fit):**

- Privileged caller required (admin, authority, upgrade authority, multi-sig) → **-25**.
- Attack path is partial (general idea is sound but cannot write exact caller → instruction → state change → outcome) → **-20**.
- Impact is self-contained (only affects the attacker's own funds, no spillover to other users) → **-15**.
- Requires specific token behavior (Token-2022 extensions, transfer hooks, interest-bearing, fee-on-transfer) that may not apply to whitelisted tokens → **-10**.
- Requires external precondition (oracle failure, bridge delay, Solana runtime version constraint) → **-10**.

Confidence indicator: `[score]` (e.g., `[95]`, `[75]`, `[60]`).

Findings below the confidence threshold (default 75) are still included in the report table but do not get a **Fix** section — description only.

## Do Not Report

- Anything a linter, compiler, or seasoned Rust developer would dismiss — INFO-level notes, gas/CU micro-optimizations, naming, documentation, redundant comments.
- Admin/authority can set fees, parameters, or pause — these are by-design privileges, not vulnerabilities.
- Missing event emissions or insufficient logging.
- Centralization observations without a concrete exploit path (e.g., "upgrade authority could rug" with no specific mechanism beyond trust assumptions).
- Theoretical issues requiring implausible preconditions (e.g., compromised Solana validator, >50% token supply held by attacker). Note: common token behaviors (Token-2022 transfer hooks, fee-on-transfer, interest-bearing) are NOT implausible — if the code accepts arbitrary tokens, these are valid attack surfaces.

## 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 -->
| **Attack vectors checked**       | 120 (across N agents)                                  |
| **Agents deployed**              | N vector-scan + adversarial + protocol                 |
| **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 findings >

---

Findings List

| # | Confidence | Title |
|---|---|---|
| 1 | [95] | <title> |
| 2 | [82] | <title> |
| | | **Below Confidence Threshold** |
| 3 | [75] | <title> |
| 4 | [60] | <title> |

---

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

````

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

