example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
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
Claude Code
$ npx -y skills add omermaksutii/RugProof --skill solana-anchor --agent claude-code
Codex
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a codex -y
Cursor
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a cursor -y
Gemini CLI
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a gemini-cli -y
GitHub Copilot
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a github-copilot -y
OpenCode
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a opencode -y
Windsurf
$ npx skills add omermaksutii/RugProof --skill solana-anchor -a windsurf -y
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 has no implicit caller. Every authority, ownership, and identity invariant must be asserted explicitly in the account struct or handler.
#[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.
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.
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.
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`.
#[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.
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.
**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.
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.
/// 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.
| Pattern | Severity | Notes | |---|---|---| | Authority as `AccountInfo`, no signer proof | **Critical** | Impersonate any authority | | Deserialize raw account, no owner check | **Critical** |
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
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the…
Use for authorized security assessment of REST, GraphQL, WebSocket, or SOAP APIs, including discovery, authentication,…
Extract and decrypt Windows DPAPI-protected secrets (Credential Manager, browser logins/cookies, Wi-Fi credentials, KeePass keys)…
Clean and finalize authorized natural-language text intended for readers by auditing suspicious invisible Unicode and rewriting…