Solana Anchor skill for Codex

Validate every account the attacker hands you. Codex writes Anchor programs where every account is validated by type or constraint, PDAs store their canonical bump, sizes come from InitSpace and arithmetic is checked. Token transfers go through token_interface with transfer_checked so Token-2022 mints work, each instruction gets a security checklist pass, and mainnet deploys or authority changes wait for your confirmation.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- solana-anchor

Installs to ~/.agents/skills/solana-anchor/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $solana-anchor, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Solana programs in Rust with the Anchor framework - account structs with init, seeds, bump, has_one and close constraints, PDAs, InitSpace sizing, CPIs to SPL Token and Token-2022 through token_interface, custom errors, events, TypeScript clients from the IDL, LiteSVM or anchor test suites, and devnet or mainnet deploys with a safe upgrade authority. Use when asked to write or review an Anchor program, add an instruction, fix an account constraint error, test with LiteSVM, or deploy and upgrade a Solana program.

Use it when

  • You are writing or extending a Solana program with Anchor
  • An instruction fails with an Anchor constraint or account error
  • A program needs SPL Token or Token-2022 transfers from a PDA
  • You are preparing a devnet or mainnet deploy or upgrade

Use something else when

How to check the result

anchor test passes with tests for each constraint failure, solana program show confirms the expected upgrade authority, and no UncheckedAccount lacks a truthful CHECK comment.

The mistake it prevents

Accepting an account without binding it by seeds or has_one to the signer, letting an attacker substitute another user's account.

~/.agents/skills/solana-anchor/SKILL.md

--- name: solana-anchor description: Builds Solana programs in Rust with the Anchor framework - account structs with init, seeds, bump, has_one and close constraints, PDAs, InitSpace… license: MIT ---

An Anchor program is correct when every account an instruction touches is validated by type or constraint (owner, signer, seeds, relationships), PDAs use stored canonical bumps, all arithmetic is checked, accounts are sized from InitSpace, and the upgrade authority is a multisig or deliberately revoked.

Inspect first

  1. Read AGENTS.md, Anchor.toml, Cargo.toml (workspace and programs/*/Cargo.toml) and package.json. Note the anchor-lang version: 0.30+ changed IDL format, introduced ctx.bumps.<name> field access and automatic account resolution in the TS client.
  2. Check toolchain: anchor --version, solana --version (Agave CLI), rustc --version. Install or switch with avm install <ver> && avm use <ver> (needs network).
  3. Match [programs.localnet] IDs in Anchor.toml to declare_id!. If they differ, run anchor keys sync and rebuild.
  4. Baseline: anchor build && anchor test before any change.

Mental model

Solana programs are stateless; state lives in accounts passed into every instruction. The attacker controls which accounts are passed. Every bug class below comes from trusting an account the program did not validate.

Anchor typeValidates
Account<'info, T>Owned by this program, discriminator matches T, deserializes
Signer<'info>Signed the transaction
Program<'info, System> / Program<'info, Token>Executable and expected program ID
Interface<'info, TokenInterface>SPL Token or Token-2022 program
InterfaceAccount<'info, Mint/TokenAccount>Token account owned by either token program
UncheckedAccount<'info>Nothing. Requires a /// CHECK: comment explaining why it is safe

Program skeleton

use anchor_lang::prelude::*;

declare_id!("Cntr1111111111111111111111111111111111111111");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let c = &mut ctx.accounts.counter;
        c.authority = ctx.accounts.authority.key();
        c.count = 0;
        c.bump = ctx.bumps.counter;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>, by: u64) -> Result<()> {
        let c = &mut ctx.accounts.counter;
        c.count = c.count.checked_add(by).ok_or(CounterError::Overflow)?;
        emit!(Incremented { authority: c.authority, count: c.count });
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + Counter::INIT_SPACE,
        seeds = [b"counter", authority.key().as_ref()],
        bump
    )]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(
        mut,
        seeds = [b"counter", authority.key().as_ref()],
        bump = counter.bump,
        has_one = authority
    )]
    pub counter: Account<'info, Counter>,
    pub authority: Signer<'info>,
}

#[account]
#[derive(InitSpace)]
pub struct Counter {
    pub authority: Pubkey,
    pub count: u64,
    pub bump: u8,
}

#[event]
pub struct Incremented { pub authority: Pubkey, pub count: u64 }

#[error_code]
pub enum CounterError {
    #[msg("Counter overflow")]
    Overflow,
}
  • space = 8 + T::INIT_SPACE: 8 bytes are the account discriminator. String and Vec fields need #[max_len(n)].
  • Store the bump at init and pass bump = account.bump later; recomputing with bump alone costs compute every call.
  • has_one = authority checks counter.authority == authority.key().

Token CPIs (SPL Token and Token-2022)

use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked};

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(seeds = [b"vault", mint.key().as_ref()], bump = vault_state.bump)]
    pub vault_state: Account<'info, VaultState>,
    #[account(mut, token::mint = mint, token::authority = vault_state, token::token_program = token_program)]
    pub vault_ata: InterfaceAccount<'info, TokenAccount>,
    #[account(mut, token::mint = mint, token::token_program = token_program)]
    pub user_ata: InterfaceAccount<'info, TokenAccount>,
    #[account(mint::token_program = token_program)]
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub user: Signer<'info>,
}

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let mint_key = ctx.accounts.mint.key();
    let bump = [ctx.accounts.vault_state.bump];
    let seeds: &[&[u8]] = &[b"vault", mint_key.as_ref(), &bump];
    let signer: &[&[&[u8]]] = &[seeds];
    let cpi = TransferChecked {
        from: ctx.accounts.vault_ata.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.user_ata.to_account_info(),
        authority: ctx.accounts.vault_state.to_account_info(),
    };
    token_interface::transfer_checked(
        CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(), cpi, signer),
        amount,
        ctx.accounts.mint.decimals,
    )
}
  • Use transfer_checked, not transfer: Token-2022 mints with extensions (transfer fees, hooks) require it.
  • Token-2022 transfer hooks need extra accounts passed through remaining_accounts.
  • In real code add the business authorization (who may withdraw how much) before the CPI.

Security checklist per instruction

  • Every account that must sign is Signer; every privileged action checks the stored authority (has_one or constraint = ...).
  • No UncheckedAccount or AccountInfo without a /// CHECK: that is actually true.
  • PDAs derived with seeds that bind them to the right user or mint (prevents account substitution between users).
  • Two mutable accounts of the same type cannot be the same account (constraint = a.key() != b.key()) when that would double-count.
  • checked_add/sub/mul or u128 intermediates; never plain + on balances (release builds wrap unless overflow-checks = true in the profile).
  • init_if_needed only when re-initialization cannot reset state an attacker benefits from.
  • Closing accounts with close = destination; never zero lamports by hand.
  • remaining_accounts validated manually (owner, key, discriminator) before use.
  • CPI targets are typed Program/Interface, never an arbitrary account.

Client (TypeScript)

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import type { Counter } from "../target/types/counter";

const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Counter as Program<Counter>;

await program.methods.initialize().rpc();                     // PDA accounts auto-resolved
await program.methods.increment(new anchor.BN(1)).rpc();
const [pda] = anchor.web3.PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), provider.wallet.publicKey.toBuffer()], program.programId);
const acct = await program.account.counter.fetch(pda);

Use .accountsPartial({...}) when you must pass accounts the resolver cannot infer. Check package.json for the client package name the repo pins; it must match anchor-lang.

Testing

OptionWhen
anchor test (local validator, TS mocha)Default for end-to-end and client code
LiteSVM (Rust or TS)Fast in-process tests, time travel, setting arbitrary account state
MolluskSingle-instruction unit tests with compute-unit measurement

Test each instruction’s success path, every constraint failure (wrong signer, wrong PDA, foreign mint, same account twice) and every error code. anchor test needs no network; deploying to devnet does.

Deploy and upgrade

anchor build --verifiable                     # reproducible build for verification
anchor deploy --provider.cluster devnet       # needs network and a funded keypair
solana program show <PROGRAM_ID> --url devnet
solana program set-upgrade-authority <PROGRAM_ID> --new-upgrade-authority <MULTISIG> --url mainnet-beta
  • Mainnet deploys and upgrade-authority changes need explicit user confirmation. Show the program ID, cluster, buffer size and authority first. --final makes a program permanently immutable; never run it without the user asking for exactly that.
  • Program size grows with features; deploys use a buffer account and cost rent in SOL. Extend with solana program extend if an upgrade outgrows the allocated space.
  • Account layout changes on upgrade: only append fields, or add a version byte and a migration instruction. Reordering fields corrupts existing accounts.

Gotchas

  • Compute: 200k CU per instruction by default, 1.4M max per transaction; request more with ComputeBudgetProgram.setComputeUnitLimit. Transactions are capped at 1232 bytes; use address lookup tables for many accounts.
  • msg! with formatting is expensive in loops; remove debug logs before measuring CU.
  • Account realloc is limited per instruction (10 KiB growth); plan large accounts upfront or use zero-copy (#[account(zero_copy)] with AccountLoader).
  • Devnet airdrops are rate-limited; keep a funded test keypair.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Solana Anchor"
  short_description: "Validate every account the attacker hands you"
  default_prompt: "$solana-anchor Write an Anchor program for a token vault with deposit and withdraw instructions"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem