| name | anchor-solana-smart-contracts-rust |
| description | Production-grade Solana smart contract (program) development using Rust and the Anchor framework. Covers security vulnerability mitigation, account validation, PDA derivation, CPI safety, and TypeScript testing. |
Anchor Solana Smart Contracts & Rust Security Guide
This skill guide provides production-grade architecture, vulnerability defenses, security patterns, anti-patterns, and testing strategies for developing Solana programs using Rust and the Anchor Framework.
1. Core Principles of Solana & Anchor Security
Unlike EVM contracts where code and state reside together in a single contract account, Solana separates code (Programs) from state (Accounts).
- Explicit Account Validation: Programs must validate every account passed into an instruction (Owner, Signer, Writable, PDA Seeds/Bump, Expected Key).
- PDA (Program Derived Address) Hardening: Never trust a client-provided bump without validating it against
Pubkey::find_program_address or storing/canonicalizing the canonical bump.
- Strict Type Safety: Enforce explicit discriminator checks to prevent account type spoofing ("Type Cosplaying").
- Checked Arithmetic: Solana execution halts on panics; always use
checked_add, checked_sub, checked_mul, or Anchor's safe math helpers.
2. Critical Solana Vulnerabilities & Security Defenses
2.1 Account Reloading & Owner Validation (Type Cosplaying)
Vulnerability
If an account struct does not validate that an account is owned by the expected program or derived with the expected Anchor account discriminator, an attacker can create a fake account with identical memory layout owned by a malicious program.
Defense
Anchor handles account discriminators automatically when using #[account] and Account<'info, T>. Never use UncheckedAccount unless strictly necessary and manually validated.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer};
declare_id!("8xYg91X29W7dK8J5161GzV41jV7W8yXz111111111111");
#[program]
pub mod secure_vault {
use super::*;
pub fn initialize_vault(ctx: Context<InitializeVault>, bump: u8) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.authority = ctx.accounts.authority.key();
vault.token_account = ctx.accounts.vault_token_account.key();
vault.bump = bump;
Ok(())
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::InvalidAmount);
let cpi_accounts = Transfer {
from: ctx.accounts.user_token_account.to_account_info(),
to: ctx.accounts.vault_token_account.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_ctx, amount)?;
let = & ctx.accounts.vault;
vault.total_deposited = vault
.total_deposited
.(amount)
.(VaultError::MathOverflow)?;
(())
}
}
<> {
,
bump
)]
vault: Account<, VaultState>,
authority: Signer<>,
mint: Account<, Mint>,
,
bump
)]
vault_token_account: Account<, TokenAccount>,
system_program: Program<, System>,
token_program: Program<, Token>,
}
<> {
,
bump = vault.bump,
has_one = vault_token_account @ VaultError::InvalidTokenAccount
)]
vault: Account<, VaultState>,
vault_token_account: Account<, TokenAccount>,
user_token_account: Account<, TokenAccount>,
user: Signer<>,
token_program: Program<, Token>,
}
{
authority: Pubkey,
token_account: Pubkey,
total_deposited: ,
bump: ,
}
{
InvalidAmount,
MathOverflow,
InvalidTokenAccount,
InvalidOwner,
InvalidMint,
}
2.2 Reinitialization & Closed Account Revival
Vulnerability
If an account is closed (lamports drained to 0) in Solana, an attacker can re-fund the account in the same transaction or block and reinitialize it if the program doesn't set a closed discriminator or verify state flags.
Defense
Anchor handles closing accounts safely via #[account(close = receiver)]. This zeroes the account memory and writes a special CLOSED_ACCOUNT_DISCRIMINATOR (8 zero bytes) to prevent reuse within the same transaction.
#[derive(Accounts)]
pub struct CloseVault<'info> {
#[account(
mut,
close = authority,
has_one = authority @ VaultError::Unauthorized,
constraint = vault.total_deposited == 0 @ VaultError::VaultNotEmpty
)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub authority: Signer<'info>,
}
2.3 PDA Seed Collision & Canonical Bump Defense
Vulnerability
Using non-unique seeds (e.g., overlapping strings or uncontrolled user inputs) can allow two distinct logic paths to resolve to the exact same Program Derived Address (PDA).
Defense Strategy
- Prefix seeds with constant byte literals (
b"user_profile").
- Include all necessary domain identifiers (e.g.,
user.key(), mint.key()).
- Always validate and store the canonical bump returned by Anchor's
find_program_address.
#[account(
init,
payer = payer,
space = 8 + UserProfile::INIT_SPACE,
seeds = [b"user_profile", user.key().as_ref()],
bump
)]
pub profile: Account<'info, UserProfile>,
2.4 Arbitrary CPI & Program ID Checks
Vulnerability
Passing an unvalidated AccountInfo as a program ID when making a Cross-Program Invocation (CPI) allows an attacker to pass a malicious program that mimics standard interfaces (e.g., fake Token Program).
Defense
Use Anchor's strongly typed Program<'info, System> or Program<'info, Token> instead of raw AccountInfo.
pub token_program: Program<'info, Token>,
3. Production Anchor Testing with TypeScript / Mocha
Testing Anchor programs involves using @coral-xyz/anchor and @solana/web3.js with local validator test runners (anchor test).
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SecureVault } from "../target/types/secure_vault";
import {
TOKEN_PROGRAM_ID,
createMint,
createAccount,
mintTo,
getAccount,
} from "@solana/spl-token";
import { assert } from "chai";
describe("secure_vault", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.SecureVault as Program<SecureVault>;
const authority = provider.wallet;
let mint: anchor.web3.PublicKey;
let userTokenAccount: anchor.web3.PublicKey;
let vaultPda: anchor.web3.PublicKey;
let vaultBump: number;
let vaultTokenPda: anchor..;
( () => {
mint = (
provider.,
(authority ).,
authority.,
,
);
[vaultPda, vaultBump] = anchor...(
[.(), authority..()],
program.
);
[vaultTokenPda] = anchor...(
[.(), vaultPda.()],
program.
);
userTokenAccount = (
provider.,
(authority ).,
mint,
authority.
);
(
provider.,
(authority ).,
mint,
userTokenAccount,
authority.,
);
});
(, () => {
program.
.(vaultBump)
.({
: vaultPda,
: authority.,
: mint,
: vaultTokenPda,
: anchor...,
: ,
})
.();
vaultAccount = program...(vaultPda);
assert.(vaultAccount..(), authority..());
});
(, () => {
depositAmount = anchor.();
program.
.(depositAmount)
.({
: vaultPda,
: vaultTokenPda,
: userTokenAccount,
: authority.,
: ,
})
.();
vaultAccount = program...(vaultPda);
assert.(vaultAccount..(), depositAmount.());
});
});
4. Solana Security Checklist