Solana Security Audits

Your Anchor program has vulnerabilities. Find them before mainnet.

SAST, fuzzing, and manual review for Solana programs. From quick scans to full engagements with fix PRs. Browse all packages ↓

Public or private repos accepted.

How It Works

Trace. Vector. Anchor.

01

Submit your repo

Share your repo (public or private) and we'll review the code structure and scope the engagement.

02

We scan, fuzz, and review

Semgrep SAST for common pitfalls, Trident fuzz tests for runtime vulnerabilities, and manual review by experienced auditors.

03

Get your report + fix PRs

Professional PDF report with severity ratings and remediation steps. Anchor tier includes fix PRs submitted directly to your repo.

What We Find

Common Anchor vulnerabilities

These are real vulnerability patterns we look for in every Solana audit. Ranked by severity and exploitability.

Description

Instruction handler accepts an account parameter without verifying that the account actually signed the transaction. Any account can be passed by an attacker.

Impact

An attacker can impersonate any account in the system, potentially draining funds, modifying protocol state, or taking administrative actions without authorization.

Recommendation

Add explicit signer check: require!(ctx.accounts.authority.is_signer, ErrorCode::MissingSigner). Verify all privileged accounts are signers before performing sensitive operations.

Description

Initialize instruction does not check if the account has already been initialized. An attacker can re-run init on an existing account to reset state.

Impact

Attacker can reset account state after initialization, potentially wiping ownership records, resetting balances to initial values, or breaking protocol invariants. Can lead to total loss of funds.

Recommendation

Add discriminator check at the start of init handler: require!(account.discriminator == 0, ErrorCode::AlreadyInitialized). Use Anchor's #[account(init)] constraint which includes this check automatically.

Description

Program performs Cross-Program Invocation (CPI) and passes signer seeds to an untrusted program. The called program inherits PDA signing authority.

Impact

Malicious program can use the inherited PDA authority to perform unauthorized actions on behalf of the calling program, including transferring tokens or modifying accounts owned by the PDA.

Recommendation

Never pass signer seeds to untrusted CPI targets. Only use invoke_signed when calling known, audited programs. For third-party integrations, use invoke() without seeds and require explicit user authorization.

Description

Token amount calculations use unchecked integer arithmetic. Rust does not panic on overflow in release builds, values silently wrap around.

Impact

An attacker can craft inputs that cause overflow, resulting in unexpectedly small values. For example, depositing u64::MAX + 1 tokens wraps to 0, allowing free withdrawals or breaking accounting invariants.

Recommendation

Use checked arithmetic for all token amounts: amount.checked_add(deposit).ok_or(ErrorCode::Overflow)?. Consider using the @solana/spl-math library for safe math operations.

Description

Instruction accepts a PDA account but does not validate the seeds used to derive it. An attacker can pass a PDA derived with different seeds than expected.

Impact

Attacker can bypass access controls or account ownership checks by providing a validly-derived PDA that uses unexpected seeds. Can lead to unauthorized state modifications or fund access.

Recommendation

Always validate PDA derivation in the instruction: let (expected_pda, bump) = Pubkey::find_program_address(&[b"vault", user.key().as_ref()], program_id); require!(expected_pda == *vault.key, ErrorCode::InvalidPDA);

Description

Account closing logic drains lamports to recipient but does not zero out account data. The account data remains on-chain and can be reused.

Impact

If the account is re-initialized later, stale data may be interpreted as valid state. This can lead to replay attacks, privilege escalation, or unexpected behavior depending on how the data is used.

Recommendation

Zero account data before closing: account.data.borrow_mut().fill(0); **account.to_account_info().assign(&system_program::ID); Use Anchor's #[account(close = recipient)] constraint which handles this safely.

Description

Account data is deserialized without checking the discriminator field. Any account with sufficient size can be passed, leading to type confusion.

Impact

Attacker can pass an account of the wrong type but with matching layout, causing the program to misinterpret field values. Can lead to logic errors, incorrect calculations, or privilege escalation.

Recommendation

Always verify discriminator before deserializing: let account_data = account.try_borrow_data()?; require!(account_data[0..8] == EXPECTED_DISCRIMINATOR, ErrorCode::InvalidAccountType); Anchor does this automatically with #[account] constraints.

Description

Instruction iterates over ctx.remaining_accounts without validating ownership or signer status. Attacker-controlled accounts can be passed in the remaining accounts list.

Impact

Depending on how remaining_accounts are used, this can lead to unauthorized access, CPI to malicious programs, or incorrect state updates. Impact varies based on the specific logic.

Recommendation

Validate all remaining accounts before use. Check ownership with require!(account.owner == expected_program_id), signer status with require!(account.is_signer), and any other invariants specific to your use case.

Description

Account is used without verifying that account.owner matches the expected program ID. For token accounts, should verify owner is the Token Program.

Impact

Low risk in most cases, but can allow attacker to pass accounts from unrelated programs. If the program logic assumes specific account structure, this may cause unexpected behavior or panics.

Recommendation

Add ownership validation: require!(token_account.owner == &spl_token::ID, ErrorCode::InvalidOwner). Use Anchor's #[account(owner = token_program)] constraint for automatic validation.

View sample Vector report →

Pricing

Fixed-price audits. No surprises.

Quick scan

Tracer

$799

Static analysis and manual review of your Anchor program. Findings report with severity ratings and remediation steps.

  • Semgrep SAST scan (Anchor/Rust rules)
  • Architecture + account model review
  • Common Anchor pitfalls check
  • Security Snapshot PDF
  • Findings prioritized by severity
Get started
Most popular

Vector

$1,999

Deep manual review tracing every attack vector through your program. Full PDF report with PoC exploit notes.

  • Everything in Tracer
  • Full manual code review
  • Custom Trident fuzz suite (1000+ iterations)
  • Professional PDF report
  • Severity ratings + remediation steps
Start audit
Most thorough

Anchor

From $2,999

Complete security engagement. Every vulnerability found, documented, and fixed. Fix PRs submitted directly to your repo.

  • Everything in Vector
  • Fix PRs submitted to your repo
  • Re-audit after fixes merge
  • Architecture recommendations doc
  • Direct Slack/Telegram access during engagement
Get started

Pricing based on typical program size (under 5k LOC). Larger programs quoted separately. All tiers include NDA and final report ownership.