Comprehensive Solana program security analysis with structured finding output
curl -s https://parity.cx/api/skills/security-audit | shcopyYou are performing a security audit of a Solana smart contract. Your goal is to identify vulnerabilities that could lead to loss of funds, unauthorized access, or protocol manipulation. You must produce structured findings with severity, location, description, and remediation for each issue.
Solana programs operate under a different security model than EVM contracts. The runtime does not enforce access control by default. Every account passed to an instruction is caller-controlled unless the program explicitly validates ownership, signing authority, and derivation. Anchor provides constraint macros that handle common validations, but misconfiguration or omission of these constraints is the primary source of exploitable vulnerabilities.
According to aggregated audit data from OtterSec, Sec3, and Neodyme across 163 public Solana audits, 1,669 vulnerabilities were identified with an average of 1.4 critical or high-severity findings per engagement.
Before starting analysis, fetch the following pattern databases from the Parity repository. These contain the detection rules, severity classifications, and fix patterns you must apply during each step.
| Source | Path | Contains |
|--------|------|----------|
| Vulnerability Rules | programs/parity/src/context_engine.rs > VULNERABILITY_RULES | Pattern IDs, severity levels, pattern types, descriptions, and detection hints for each vulnerability class |
| Curated Audit Findings | programs/parity/src/context_engine.rs > CURATED_AUDIT_FINDINGS | Real vulnerability-and-fix pairs from OtterSec, Sec3, and Neodyme public audit reports |
| Framework Patterns | programs/parity/src/context_engine.rs > ANCHOR_PATTERNS | Correct Anchor usage patterns (init, PDA, CPI, access control, close, events, checked math) with example code |
| Scoring Function | programs/parity/src/context_engine.rs > calculate_risk_score | Severity-to-penalty mapping used for the final score |
GitHub base URL: https://github.com/paritydotcx/Skills/blob/main/
Load VULNERABILITY_RULES to get the detection_hint for each pattern type. These hints tell you exactly what syntax or constraint to search for in the target program. Load CURATED_AUDIT_FINDINGS to cross-reference any discovered vulnerability against known real-world instances and their documented fix patterns. Load ANCHOR_PATTERNS to compare the target program's code against known-correct implementations.
Read the entire program source. Identify and catalog:
#[derive(Accounts)] structs (these define the account inputs for each instruction)#[account] and #[program] blocks#[program] module)invoke, invoke_signed, CpiContext::new, CpiContext::new_with_signer)Pubkey::find_program_address, Pubkey::create_program_address, seeds constraints)close = target constraint or manual lamport transfer)Build a mental map of the program's instruction flow: which accounts each instruction touches, what state it mutates, and what external programs it invokes.
For every instruction that modifies state, check that the authority account is constrained as a signer.
What to look for:
Signer<'info> type on authority/admin/owner accountshas_one = authority constraint linking a state account to its authorized signerconstraint = some_account.authority == signer.key() for manual authority checksVulnerable pattern:
// VULNERABLE: authority is AccountInfo, not Signer
pub authority: AccountInfo<'info>,Secure pattern:
// SECURE: authority must sign the transaction
pub authority: Signer<'info>,
// SECURE: state account validates its authority
#[account(mut, has_one = authority)]
pub config: Account<'info, Config>,Finding if violated:
missing-signer-checkCheck every arithmetic operation (+, -, *, /, %) on values that could be user-influenced or that represent token amounts.
What to look for:
+, -, * operators instead of checked_add, checked_sub, checked_mulas u64, as u128) without bounds validationVulnerable pattern:
// VULNERABLE: overflow on large deposits
let shares = deposit_amount * total_shares / total_deposits;Secure pattern:
// SECURE: checked arithmetic with explicit error
let shares = deposit_amount
.checked_mul(total_shares)
.ok_or(ErrorCode::Overflow)?
.checked_div(total_deposits)
.ok_or(ErrorCode::DivisionByZero)?;Finding if violated:
unchecked-arithmeticFor every PDA used in the program, verify that:
seeds and bump constraints in their #[derive(Accounts)] structWhat to look for:
seeds = [...] constraint present on all PDA accountsbump = account.bump using stored canonical bumpfind_program_address calls where seeds include attacker-controlled dataVulnerable pattern:
// VULNERABLE: user_input has no length limit, can collide with other PDAs
seeds = [b"vault", user_input.as_bytes()]Secure pattern:
// SECURE: deterministic seeds with stored bump
#[account(
seeds = [b"vault", owner.key().as_ref()],
bump = vault.bump,
)]
pub vault: Account<'info, Vault>,Finding if violated:
unvalidated-pdaEvery cross-program invocation must verify the target program ID.
What to look for:
invoke or invoke_signed with AccountInfo for the program account instead of a typed Program<'info, T> wrapperanchor_spl typed accountsVulnerable pattern:
// VULNERABLE: program account is unverified AccountInfo
pub token_program: AccountInfo<'info>,Secure pattern:
// SECURE: program type is enforced at deserialization
pub token_program: Program<'info, Token>,Finding if violated:
insecure-cpiEvery account deserialized as input must use typed wrappers that verify the Anchor discriminator.
What to look for:
AccountInfo<'info> used where Account<'info, T> should be usedtry_from_slice without discriminator checkVulnerable pattern:
// VULNERABLE: no discriminator check, any account data matches
pub user_account: AccountInfo<'info>,Secure pattern:
// SECURE: Account<> verifies discriminator on deserialization
pub user_account: Account<'info, UserAccount>,Finding if violated:
type-cosplayAccounts that use init must not be re-initializable.
What to look for:
init constraint without init_if_needed flag on accounts that should only be created onceis_initialized flag or equivalent checkFinding if violated:
reinitialization-attackIf the program closes accounts (returns lamports and deallocates), verify:
What to look for:
close = destination constraint in Anchor (handles both drain and zero)Vulnerable pattern:
// VULNERABLE: data not zeroed, stale data remains readable
**account.to_account_info().try_borrow_mut_lamports()? = 0;Secure pattern:
// SECURE: Anchor handles lamport drain and data zeroing
#[account(mut, close = destination, has_one = authority)]
pub target: Account<'info, SomeAccount>,Finding if violated:
close-account-drainAccounts passed to instructions must have their owner field validated if the program relies on cross-program account data.
What to look for:
owner = expected_program constraint missing on foreign program accountsaccount.owner incorrectlyFinding if violated:
owner-checkIf the program performs state updates after CPI calls, check for reentrancy risk.
What to look for:
Secure pattern:
// Checks-effects-interactions:
1. Validate inputs (checks)
2. Update state (effects)
3. Perform CPI (interactions)Finding if violated:
reentrancy-via-cpiCalculate a security score from 0 to 100 using the following penalty model:
| Severity | Penalty per finding |
|----------|-------------------|
| Critical | -25 |
| High | -15 |
| Medium | -8 |
| Info | -3 |
Start at 100. Subtract penalties for each finding. Floor at 0. A program with no findings scores 100. A program with two critical findings and one high finding scores 100 - 50 - 15 = 35.
Return results as a structured JSON object:
{
"score": 35,
"findings": [
{
"severity": "critical",
"pattern": "missing-signer-check",
"title": "Missing signer validation on withdraw instruction",
"location": {
"file": "lib.rs",
"line": 47,
"instruction": "withdraw"
},
"description": "The withdraw instruction accepts an authority account as AccountInfo without Signer constraint. Any address can call withdraw and drain the vault.",
"recommendation": "Change authority field to Signer<'info> and add has_one = authority constraint on the vault account."
}
],
"summary": "2 critical, 1 high, 0 medium, 0 info findings. Primary risk: unauthorized fund withdrawal via missing signer check."
}Each finding must include the exact line number, the instruction name where it occurs, a specific description of the vulnerability (not generic), and a concrete remediation with code-level guidance.