Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scans Solana programs for 6 critical vulnerabilities including arbitrary CPI, improper PDA validation, missing signer/ownership checks, and sysvar spoofing. Use when auditing Solana/Anchor programs.
.claude/skills/trailofbits-solana-vulnerability-scanner/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 39% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 153% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 115% | 0% |
Systematically scan Solana programs (native and Anchor framework) for platform-specific security vulnerabilities related to cross-program invocations, account validation, and program-derived addresses. This skill encodes 6 critical vulnerability patterns unique to Solana's account model.
.rsrust// Native Solana program indicators use solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, program::invoke, program::invoke_signed, }; entrypoint!(process_instruction); // Anchor framework indicators use anchor_lang::prelude::*; #[program] pub mod my_program { pub fn initialize(ctx: Context<Initialize>) -> Result<()> { // Program logic } } #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] pub authority: Signer<'info>, } // Common patterns AccountInfo, Pubkey invoke(), invoke_signed() Signer<'info>, Account<'info> #[account(...)] with constraints seeds, bump
programs/*/src/lib.rs - Program implementationAnchor.toml - Anchor configurationCargo.toml with solana-program or anchor-langtests/ - Program testsWhen invoked, I will:
I check for 6 critical vulnerability patterns unique to Solana. For detailed detection patterns, code examples, mitigations, and testing strategies, see VULNERABILITY_PATTERNS.md.
For complete vulnerability patterns with code examples, see VULNERABILITY_PATTERNS.md.
programs/*/src/lib.rs)bash# Find all CPI calls rg "invoke\(|invoke_signed\(" programs/ # Check for program ID validation before each # Should see program ID checks immediately before invoke
For each CPI:
Program<'info, T> typebash# Find PDA usage rg "find_program_address|create_program_address" programs/ rg "seeds.*bump" programs/ # Anchor: Check for seeds constraints rg "#\[account.*seeds" programs/
For each PDA:
find_program_address() or Anchor seeds constraintbash# Find account deserialization rg "try_from_slice|try_deserialize" programs/ # Should see owner checks before deserialization rg "\.owner\s*==|\.owner\s*!=" programs/
For each account used:
Account<'info, T> and Signer<'info>bash# Find instruction introspection usage rg "load_instruction_at|load_current_index|get_instruction_relative" programs/ # Check for checked versions rg "load_instruction_at_checked|load_current_index_checked" programs/
toml# Add to Cargo.toml [dependencies] solana-program = "1.17" # Use latest version [lints.clippy] # Enable Solana-specific lints # (Trail of Bits solana-lints if available)
markdown## [CRITICAL] Arbitrary CPI - Unchecked Program ID **Location**: `programs/vault/src/lib.rs:145-160` (withdraw function) **Description**: The `withdraw` function performs a CPI to transfer SPL tokens without validating that the provided `token_program` account is actually the SPL Token program. An attacker can provide a malicious program that appears to perform a transfer but actually steals tokens or performs unauthorized actions. **Vulnerable Code**:
// lib.rs, line 145 pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { let token_program = &ctx.accounts.token_program;
// WRONG: No validation of token_program.key()! invoke( &spl_token::instruction::transfer(...), & ctx.accounts.vault.to_account_info(), ctx.accounts.destination.to_account_info(), ctx.accounts.authority.to_account_info(), token_program.to_account_info(), // UNVALIDATED ], )?; Ok(()) }
**Attack Scenario**:
1. Attacker deploys malicious "token program" that logs transfer instruction but doesn't execute it
2. Attacker calls withdraw() providing malicious program as token_program
3. Vault's authority signs the transaction
4. Malicious program receives CPI with vault's signature
5. Malicious program can now impersonate vault and drain real tokens
**Recommendation**:
Use Anchor's `Program<'info, Token>` type:use anchor_spl::token::{Token, Transfer};
#derive(Accounts)] pub struct Withdraw<'info> { #account(mut)] pub vault: Account<'info, TokenAccount>, #account(mut)] pub destination: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>, // Validates program ID automatically }
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.authority.to_account_info(), };
let cpi_ctx = CpiContext::new( ctx.accounts.token_program.to_account_info(), cpi_accounts, );
anchor_spl::token::transfer(cpi_ctx, amount)?; Ok(()) }
**References**:
- building-secure-contracts/not-so-smart-contracts/solana/arbitrary_cpi
- Trail of Bits lint: `unchecked-cpi-program-id`rust#[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn test_rejects_wrong_program_id() { // Provide wrong program ID, should fail } #[test] #[should_panic] fn test_rejects_non_canonical_pda() { // Provide non-canonical bump, should fail } #[test] #[should_panic] fn test_requires_signer() { // Call without signature, should fail } }
typescriptimport * as anchor from "@coral-xyz/anchor"; describe("security tests", () => { it("rejects arbitrary CPI", async () => { const fakeTokenProgram = anchor.web3.Keypair.generate(); try { await program.methods .withdraw(amount) .accounts({ tokenProgram: fakeTokenProgram.publicKey, // Wrong program }) .rpc(); assert.fail("Should have rejected fake program"); } catch (err) { // Expected to fail } }); });
bash# Run local validator for testing solana-test-validator # Deploy and test program anchor test
building-secure-contracts/not-so-smart-contracts/solana/Before completing Solana program audit:
CPI Security (CRITICAL):
invoke()Program<'info, T> typePDA Security (CRITICAL):
find_program_address() or Anchor seeds constraintAccount Validation (HIGH):
account.owner == expected_program_idAccount<'info, T> typeSigner Validation (CRITICAL):
is_signeraccount.is_signer == trueSigner<'info> typeSysvar Security (HIGH):
load_instruction_at_checked()Instruction Introspection (MEDIUM):
Testing:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | pass→pass | 8,870 | 9,954 | +12% | 1 | 1 | 0% | 1,940 | 4,912 | +153% | 0 | 0 | — |
case-03 | fail→fail | 12,083 | 11,267 | -7% | 1 | 1 | 0% | 1,185 | 3,977 | +236% | 0 | 0 | — |
case-01 | fail→fail | 8,439 | 9,451 | +12% | 1 | 1 | 0% | 901 | 3,656 | +306% | 0 | 0 | — |
case-02 | fail→fail | 12,198 | 8,760 | -28% | 1 | 1 | 0% | 1,136 | 3,637 | +220% | 0 | 0 | — |
case-05 | pass→pass | 9,782 | 7,782 | -20% | 1 | 1 | 0% | 2,129 | 4,583 | +115% | 0 | 0 | — |
case-06 | pass→pass | 12,895 | 12,093 | -6% | 1 | 1 | 0% | 2,714 | 5,575 | +105% | 0 | 0 | — |
case-07 | pass→pass | 8,984 | 5,604 | -38% | 1 | 1 | 0% | 1,632 | 4,004 | +145% | 0 | 0 | — |
case-08 | pass→pass | 13,277 | 11,187 | -16% | 1 | 1 | 0% | 2,360 | 4,970 | +111% | 0 | 0 | — |
case-09 | fail→fail | 5,353 | 5,783 | +8% | 1 | 1 | 0% | 1,091 | 3,921 | +259% | 0 | 0 | — |
case-10 | fail→fail | 9,153 | 6,937 | -24% | 1 | 1 | 0% | 1,711 | 4,310 | +152% | 0 | 0 | — |
case-11 | fail→pass | 11,820 | 4,764 | -60% | 1 | 1 | 0% | 2,412 | 3,777 | +57% | 0 | 0 | — |
case-12 | pass→pass | 12,034 | 10,168 | -16% | 1 | 1 | 0% | 2,206 | 4,856 | +120% | 0 | 0 | — |
case-13 | pass→pass | 3,955 | 3,664 | -7% | 1 | 1 | 0% | 685 | 3,539 | +417% | 0 | 0 | — |
case-14 | pass→pass | 5,513 | 4,745 | -14% | 1 | 1 | 0% | 980 | 3,653 | +273% | 0 | 0 | — |
case-15 | pass→pass | 14,350 | 13,385 | -7% | 1 | 1 | 0% | 2,655 | 5,495 | +107% | 0 | 0 | — |
case-16 | pass→pass | 10,536 | 7,795 | -26% | 1 | 1 | 0% | 1,957 | 4,280 | +119% | 0 | 0 | — |
case-17 | pass→pass | 11,318 | 5,460 | -52% | 1 | 1 | 0% | 2,215 | 3,767 | +70% | 0 | 0 | — |
case-18 | fail→pass | 14,509 | 4,553 | -69% | 1 | 1 | 0% | 2,729 | 3,784 | +39% | 0 | 0 | — |
case-19 | pass→pass | 13,261 | 5,886 | -56% | 1 | 1 | 0% | 2,583 | 3,960 | +53% | 0 | 0 | — |
case-20 | fail→pass | 13,602 | 7,462 | -45% | 1 | 1 | 0% | 2,459 | 4,225 | +72% | 0 | 0 | — |
case-21 | pass→pass | 9,510 | 3,250 | -66% | 1 | 1 | 0% | 1,765 | 3,581 | +103% | 0 | 0 | — |
case-22 | pass→pass | 3,731 | 4,245 | +14% | 1 | 1 | 0% | 705 | 3,612 | +412% | 0 | 0 | — |
case-23 | pass→pass | 10,529 | 8,139 | -23% | 1 | 1 | 0% | 2,133 | 4,604 | +116% | 0 | 0 | — |
case-24 | pass→pass | 6,584 | 3,004 | -54% | 1 | 1 | 0% | 1,207 | 3,427 | +184% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.