Skip to content
Security
Skill

/solana-anchor

Detect bug classes specific to Solana / Anchor (Rust) programs — missing signer checks, missing account owner checks, account-confusion / type-cosplay without discriminator validation, unchecked AccountInfo, non-canonical PDA seeds/bumps & seed collisions, missing

From plugin
rugproof
952 skills23 agents45 commands4 hooks
Install
$ npx -y skills add omermaksutii/RugProof --skill solana-anchor --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/solana-anchor

Context preview

The summary Claude sees to decide when to auto-load this skill.

Detect bug classes specific to Solana / Anchor (Rust) programs — missing signer checks, missing account owner checks, account-confusion / type-cosplay without discriminator validation, unchecked AccountInfo, non-canonical PDA seeds/bumps & seed collisions, missing

SKILL.md

solana-anchor.SKILL.md
name: solana-anchor
description: Detect bug classes specific to Solana / Anchor (Rust) programs — missing signer checks, missing account owner checks, account-confusion / type-cosplay without discriminator validation, unchecked AccountInfo, non-canonical PDA seeds/bumps & seed collisions, missing has_one/constraint, CPI to unverified programs, close-account lamport-drain & revival, sysvar spoofing, and arbitrary-account substitution. Activate on any `.rs` file with `use anchor_lang`, `#[program]`, `#[derive(Accounts)]`, `#[account]`, `Signer<'info>`, or `AccountInfo`.

Solana / Anchor (Rust) detection

When this applies

  • Any `.rs` file importing `anchor_lang` (`use anchor_lang::prelude::*`)
  • Macros `#[program]`, `#[derive(Accounts)]`, `#[account]`, `#[instruction(...)]`
  • Account wrappers `Signer<'info>`, `Account<'info, T>`, `AccountInfo<'info>`, `UncheckedAccount`, `Program<'info, T>`, `Sysvar<'info, T>`
  • PDA derivation: `seeds = [...]`, `bump`, `find_program_address`, `create_program_address`
  • CPI: `CpiContext`, `invoke`, `invoke_signed`, `*_cpi`

Solana has no implicit caller. Every authority, ownership, and identity invariant must be asserted explicitly in the account struct or handler.

Detection patterns

Missing signer check (CRITICAL)

#[derive(Accounts)]
pub struct Withdraw<'info> {
    pub authority: AccountInfo<'info>,   // ← not Signer; nobody proves they're authority
    #[account(mut)]
    pub vault: Account<'info, Vault>,
}

**Signal:** an "authority"/"owner"/"admin" account typed `AccountInfo`/`UncheckedAccount` instead of `Signer<'info>`, or a handler reading `ctx.accounts.x` without checking `x.is_signer`. Anyone passes the real authority's pubkey without their signature.

Missing owner check / arbitrary account substitution (CRITICAL)

let data = ctx.accounts.state.to_account_info();
let state = State::try_from_slice(&data.data.borrow())?;  // ← no owner check

**Signal:** deserializing from a raw `AccountInfo`/`UncheckedAccount` without verifying `account.owner == program_id`, or using `Account<T>` for data but not constraining which account. An attacker passes a fake account they own with crafted bytes. `Account<'info, T>` checks owner+discriminator; raw `AccountInfo` checks nothing.

Type cosplay / missing discriminator validation (HIGH)

let user: UserAccount = UserAccount::try_from_slice(&info.data.borrow())?;  // ← any 8-byte

**Signal:** `try_from_slice` / `try_deserialize_unchecked` on raw data, bypassing the Anchor 8-byte discriminator. A different account type with the same layout (e.g. `Config` passed where `User` is expected) is accepted. Use `Account<'info, T>` so Anchor enforces the discriminator.

Non-canonical PDA bump / seed collision (HIGH)

let (pda, _bump) = Pubkey::create_program_address(
    &[b"vault", user.key().as_ref(), &[user_supplied_bump]],  // ← attacker bump
    program_id,
)?;

**Signal:** `create_program_address` with a caller-supplied bump (multiple valid PDAs → forgeable accounts), bumps not stored/re-checked against the canonical one, or seeds that omit a discriminator so `["vault", x]` collides with `["vault", x]` of another domain. Use `find_program_address` (canonical bump) or Anchor `seeds`+`bump` and persist `bump`.

Missing has_one / constraint linking accounts (HIGH)

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]                       // ← no has_one = authority
    pub config: Account<'info, Config>,
    pub authority: Signer<'info>,         // signs, but is never tied to config.authority
}

**Signal:** a `Signer` that's never bound to the data account it should control — no `#[account(has_one = authority)]` or `#[account(constraint = config.authority == authority.key())]`. Any signer updates any config. Same for `mint`, `owner`, `vault` relationships.

CPI to unverified program (HIGH)

let cpi = CpiContext::new(ctx.accounts.token_program.to_account_info(), ...);
token::transfer(cpi, amount)?;   // ← token_program is an unconstrained AccountInfo

**Signal:** `CpiContext::new` whose program account isn't constrained to the expected `Program<'info, Token>` / known program id, or `invoke`/`invoke_signed` to a caller-supplied program. Attacker passes a malicious program at the `token_program` slot. Type it `Program<'info, Token>` or assert the key.

close-account lamport drain / revival (HIGH)

**ctx.accounts.dest.lamports.borrow_mut() += account.lamports();
**account.lamports.borrow_mut() = 0;        // ← manual close, no discriminator zeroing

**Signal:** manual lamport zeroing without wiping the discriminator/data, or `#[account(close = ...)]` whose data the attacker can re-fund in the same tx (revival attack) before it's GC'd. Use Anchor `close = recipient` (it zeroes the discriminator) and don't reuse closed accounts in the same instruction.

Sysvar / clock spoofing (MEDIUM-HIGH)

pub clock: AccountInfo<'info>,            // ← passed as raw account, not Sysvar
let now = Clock::from_account_info(&ctx.accounts.clock)?.unix_timestamp;

**Signal:** Clock/Rent/Instructions sysvars typed as `AccountInfo`/`UncheckedAccount` rather than `Sysvar<'info, Clock>` or fetched via `Clock::get()`. An attacker substitutes a fake sysvar account with controlled time/data.

Unchecked AccountInfo used for value flow (MEDIUM)

/// CHECK: comment present but the account is then trusted for transfers
pub recipient: UncheckedAccount<'info>,

**Signal:** `UncheckedAccount`/`AccountInfo` (or a `/// CHECK:` with no real validation) that's subsequently used as a transfer destination, authority, or data source. The `CHECK` comment silences Anchor but doesn't add a constraint.

Severity rubric

| Pattern | Severity | Notes | |---|---|---| | Authority as `AccountInfo`, no signer proof | **Critical** | Impersonate any authority | | Deserialize raw account, no owner check | **Critical** |

Read more
Ships withrugproof

Rugproof your code before someone else does. 🌐 Live site: omermaksutii.github.io/RugProof 📦 Latest: v1.0.0 — 45 commands · 23 agents · 45 skills · 13 MCP servers · tested, offline-first, with rule packs, a benchmark, non-EVM coverage, and post-deploy

Get the whole plugin

Other skills on rugproof.