Solana Security: The Father-Son-Bank Story
2026-01-31 ยท Web3 Security ยท 15 min read
#solana #security #anchor #pinocchio #rust #blockchain #smart-contracts #vulnerabilities #web3
Let me tell you a story, where every vulnerability maps to a failure in a simple trust model: a Father authorizing transactions, his Son (a PDA), and a Bank (the program) that should verify everything.

- Father = Signer: Must authorize all sensitive operations
- Son = PDA: Derived from Father, owned by program
- Bank = Program: Validates and processes all requests
- 6 attack patterns map to failures in this trust model
- Two frameworks: Anchor (high-level) & Pinocchio (low-level)
Why another security guide?
Solana exploits have cost hundreds of millions of dollars. The Wormhole hack ($326M), Cashio ($52M), and countless smaller exploits share common patterns. Yet developers keep making the same mistakes because security docs read like legal contracts.
- The problem: Security checks feel abstract until you see them exploited.
- The solution: Map each vulnerability to a real-world scenario you can visualize.
- The result: 6 attack patterns, 6 story scenes, 32 runnable tests (16 Anchor + 16 Pinocchio).
The Story: Father, Son, and Bank
- ๐จ Father = Authority/Signer (must authorize transactions)
- ๐ฆ Son = PDA Account (derived from Father, owned by program)
- ๐ฆ Bank = Solana Program (validates and processes requests)
- ๐ฐ Treasury = Vault Account (stores value, has an owner field)
Every security vulnerability is a failure of the Bank to verify something. Let's look at three critical examples.
1. Unsigned Allowance Claim (Missing Signer)
A stranger walks into the Bank and says "The Father sent me to withdraw his money." The vulnerable Bank says "Okay, here's the money!" without checking ID.
#[derive(Accounts)]pub struct ClaimCheckVulnerable<'info> {#[account(mut)]pub treasury: Account<'info, BankTreasury>,/// CHECK: DANGEROUS - Anyone can pass any pubkey!pub father: AccountInfo<'info>, // โ No signature required#[account(mut)]pub son: Account<'info, Son>,}pub fn claim(ctx: Context<ClaimCheckVulnerable>, amount: u64) -> Result<()> {// No signature verification - ANYONE can call this!ctx.accounts.treasury.balance -= amount;ctx.accounts.son.allowance += amount;Ok(())}
#[derive(Accounts)]pub struct ClaimCheckSecure<'info> {#[account(mut)]pub treasury: Account<'info, BankTreasury>,pub father: Signer<'info>, // โ MUST sign the transaction#[account(mut,seeds = [b"son", father.key().as_ref()],bump,constraint = son.father == father.key() @ CustomError::NotFathersSon)]pub son: Account<'info, Son>,}
Use Signer<'info> instead of AccountInfo<'info> for all authority accounts. The transaction will fail if the account hasn't signed.
2. Fake Account Injection (Unsafe PDA)
Someone claims "I am the Father's Son, give me access to the family account." The vulnerable Bank says "You say you're a Son? Okay!" without checking the birth certificate (PDA derivation).
#[derive(Accounts)]pub struct AccessSonVulnerable<'info> {#[account(mut)]pub son: Account<'info, Son>,// โ No PDA verification! Any Son account is accepted!}pub fn access(ctx: Context<AccessSonVulnerable>) -> Result<()> {// Accepts ANY Son account, not verified to belong to callerctx.accounts.son.allowance = 100;Ok(())}
#[derive(Accounts)]pub struct AccessSonSecure<'info> {#[account(mut,seeds = [b"son", authority.key().as_ref()], // โ Expected derivationbump, // โ Validates PDAconstraint = son.father == authority.key() @ CustomError::NotFathersSon)]pub son: Account<'info, Son>,pub authority: Signer<'info>, // โ Must be the Father}
Always use seeds + bump constraints for PDA accounts. PDAs are only secure when you verify their derivation.
3. Allowance Overflow (Integer Overflow)
Son's allowance is 100. He asks for 18,446,744,073,709,551,615 more. The vulnerable Bank uses wrapping_add and the number wraps around to 99!
pub fn add_allowance(ctx: Context<AllowanceVulnerable>, amount: u64) -> Result<()> {let son = &mut ctx.accounts.son;// โ wrapping_add wraps on overflow!son.allowance = son.allowance.wrapping_add(amount);// Example: 1 + u64::MAX = 0 (wrapped!)Ok(())}
pub fn add_allowance(ctx: Context<AllowanceSecure>, amount: u64) -> Result<()> {let son = &mut ctx.accounts.son;// โ checked_add returns None on overflowson.allowance = son.allowance.checked_add(amount).ok_or(CustomError::ArithmeticOverflow)?;Ok(())}
Always use checked_add(), checked_sub(), etc. for financial calculations. Rust's default arithmetic wraps silently in release mode!
Anchor vs Pinocchio: Same Security, Different Styles
The examples above use Anchor, Solana's most popular framework. But what if you need maximum performance and minimal binary size? That's where Pinocchio comes in โ a low-level framework that gives you direct control over Solana primitives.
| Aspect | Anchor | Pinocchio |
|---|---|---|
| Abstraction | High-level macros | Raw Solana primitives |
| Signer Check | Signer<'info> type | account.is_signer() |
| PDA Validation | seeds + bump constraint | derive_address() + comparison |
| Binary Size | ~200KB+ | ~30KB |
| Learning Curve | Easier | Steeper |
Here's the same Unsigned Allowance Claim vulnerability in Pinocchio. Notice how you must manually check everything that Anchor does automatically:
/// VULNERABLE: Claims allowance without verifying Father's signaturepub fn claim_allowance(_father: &AccountView, // Father account (never checked!)son_account: &AccountView,amount: u64,) -> ProgramResult {// โ NO SIGNER CHECK - father.is_signer() never called!let son_data = son_account.try_borrow()?;let current_allowance = Son::read_allowance(&son_data)?;drop(son_data);if amount > current_allowance {return Err(ProgramError::InsufficientFunds);}let mut son_data = son_account.try_borrow_mut()?;Son::write_allowance(&mut son_data, current_allowance - amount)?;log!("VULNERABLE: Allowance claimed without signature!");Ok(())}
/// SECURE: Claims allowance WITH Father's signature verificationpub fn claim_allowance(father: &AccountView,son_account: &AccountView,amount: u64,) -> ProgramResult {// โ SIGNER CHECK - Father must have signed the transactionif !father.is_signer() {log!("SECURE: Rejecting - Father signature required!");return Err(ProgramError::MissingRequiredSignature);}let son_data = son_account.try_borrow()?;let current_allowance = Son::read_allowance(&son_data)?;drop(son_data);if amount > current_allowance {return Err(ProgramError::InsufficientFunds);}let mut son_data = son_account.try_borrow_mut()?;Son::write_allowance(&mut son_data, current_allowance - amount)?;log!("SECURE: Allowance claimed with verified Father signature");Ok(())}
In Anchor, using Signer<'info> automatically fails if the account didn't sign. In Pinocchio, you get an AccountView and must call is_signer() yourself. Forget the check = vulnerability.
PDA verification in Pinocchio is even more manual โ you must re-derive the address and compare it yourself:
/// SECURE: Accesses Son account WITH PDA derivation verificationpub fn access_son_account(program_id: &Address,father: &AccountView,son_account: &AccountView,) -> ProgramResult {// โ Re-derive the expected PDA addresslet mut found_pda = false;for bump in (0..=255u8).rev() {let seeds = &[b"son",father.address().as_array().as_slice(),&[bump],];let derived = pinocchio_pubkey::derive_address(seeds, None, program_id.as_array());// โ Compare provided account with expected PDAif son_account.address().as_array() == &derived {found_pda = true;break;}}if !found_pda {log!("SECURE: Rejecting - Son account not at expected PDA!");return Err(ProgramError::InvalidSeeds);}log!("SECURE: Son account verified at correct PDA");Ok(())}
Choose Pinocchio when you need maximum CU efficiency,minimal binary size, or are building performance-critical programs. The security patterns are the same โ you just implement them manually.
All 6 Vulnerabilities at a Glance
| # | Attack | Vulnerable | Secure |
|---|---|---|---|
| 1 | Unsigned Claim | AccountInfo | Signer |
| 2 | Treasury Takeover | No owner check | require!(owner == signer) |
| 3 | Fake Injection | No PDA check | seeds + bump |
| 4 | Overflow | wrapping_add | checked_add |
| 5 | Malicious CPI | Any program | Program<T> or whitelist |
| 6 | Twin Fraud | No duplicate check | a.key() != b.key() |
Run the Full Security Template
This post covers 3 of the 6 vulnerabilities. The full repository includes all attack patterns with 32 runnable tests โ 16 in Anchor (TypeScript) and 16 in Pinocchio (Rust with LiteSVM).
# Clone the repositorygit clone https://github.com/Ovodo/solana-security-template.gitcd solana-security-template# === ANCHOR (TypeScript tests) ===anchor build --program-name security_templateanchor test --program-name security_template# โ 16 passing tests# === PINOCCHIO (Rust + LiteSVM tests) ===cd programs/pinocchiocargo test# โ 16 passing tests# Expected output for each:# 1. Unsigned Allowance Claim (3 tests)# 2. Treasury Takeover (4 tests)# 3. Fake Account Injection (3 tests)# 4. Allowance Overflow (3 tests)# 5. Twin Account Fraud (3 tests)
- 6 vulnerable patterns with exploitable code (Anchor + Pinocchio)
- 6 secure patterns with best practices (both frameworks)
- 16 TypeScript tests for Anchor
- 16 Rust tests for Pinocchio (using LiteSVM)
- DEEP_DIVE.md with full documentation per vulnerability
- docs/ folder with Anchor vs Pinocchio comparisons for each pattern
- Story-based naming: modules like
unsigned_allowance_claim,treasury_takeover,fake_account_injection
Key Takeaways
- Solana security is explicit: unlike Ethereum's msg.sender, you must verify signers, owners, PDAs, and programs manually.
- Anchor helps but doesn't auto-protect: you must add constraints, use the right types, and think about attack vectors.
- Pinocchio requires even more vigilance: every check that Anchor automates must be written by hand.
- Story-based thinking works: if you can't explain who the Father is and why the Bank should check, you probably have a vulnerability.
- Test attack scenarios: every secure pattern should have a test showing the attack being rejected.
Remember: The Bank's job is to verify everything. Never trust, always verify โ whether you're using Anchor macros or writing raw Pinocchio checks.
๐จ Father must sign โ ๐ฆ Son must be verified โ ๐ฆ Bank must check everything โ ๐ฐ Treasury stays safe