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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- solana-anchor Installs to ~/.agents/skills/solana-anchor/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install solana-anchor The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project solana-anchor Installs to .agents/skills/solana-anchor/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/solana-anchor.zipunzip -o solana-anchor.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) solana-anchor PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The contract targets an EVM chain: use solidity-contracts
- You are building on Sui or Aptos Move: use move-language
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.
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
- Read
AGENTS.md,Anchor.toml,Cargo.toml(workspace andprograms/*/Cargo.toml) andpackage.json. Note theanchor-langversion: 0.30+ changed IDL format, introducedctx.bumps.<name>field access and automatic account resolution in the TS client. - Check toolchain:
anchor --version,solana --version(Agave CLI),rustc --version. Install or switch withavm install <ver> && avm use <ver>(needs network). - Match
[programs.localnet]IDs inAnchor.tomltodeclare_id!. If they differ, runanchor keys syncand rebuild. - Baseline:
anchor build && anchor testbefore 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 type | Validates |
|---|---|
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.StringandVecfields need#[max_len(n)].- Store the bump at init and pass
bump = account.bumplater; recomputing withbumpalone costs compute every call. has_one = authoritycheckscounter.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, nottransfer: 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_oneorconstraint = ...). - No
UncheckedAccountorAccountInfowithout 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/muloru128intermediates; never plain+on balances (release builds wrap unlessoverflow-checks = truein the profile). -
init_if_neededonly when re-initialization cannot reset state an attacker benefits from. - Closing accounts with
close = destination; never zero lamports by hand. -
remaining_accountsvalidated 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
| Option | When |
|---|---|
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 |
| Mollusk | Single-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.
--finalmakes 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 extendif 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)]withAccountLoader). - 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.yaml
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.