example-fork-detection
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
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
$ npx -y skills add omermaksutii/RugProof --skill solana-anchor --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/solana-anchorContext 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
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
Repo: omermaksutii/RugProof
TEMPLATE — replace with the description of your rule. Should activate on the specific code patterns your fork has. Activate on `<your trigger keywords or…
Detect unsafe assumptions about Solady's gas-optimized ERC20/ERC2612 permit and DN404 metadata. Solady's ERC20 uses custom storage slots, returns bools via…
Detect front-runnable ownership initialization in Solady Ownable / OwnableRoles. Solady's `_initializeOwner` is a guarded one-time setter (it reverts with…
Detect Solady SafeTransferLib calls that assume the token has code. SafeTransferLib.safeTransfer/safeTransferFrom/safeApprove deliberately skip the EXTCODESIZE…
Detect Uniswap V4 hooks that fail to settle currency deltas with the PoolManager. Every credit/debit a hook creates (BeforeSwapDelta, afterSwap hookDelta,…
Detect Uniswap V4 hooks whose address-encoded permission flags don't match the callbacks the hook actually implements. In V4 the hook's permissions live in the…