A comprehensive, hands-on educational resource demonstrating common Solana/Anchor security vulnerabilities and their mitigations.
SolSec Workshop is an interactive security education platform that teaches developers how to identify, exploit, and fix common vulnerabilities in Solana programs. Each module contains:
- 🔴 Vulnerable code with intentional security flaws
- 🟢 Secure code demonstrating proper mitigations
- 📝 Comprehensive tests that explain attack vectors
- 📚 Deep-dive documentation for thorough understanding
- 🏆 CTF challenges to practice your skills
- Smart contract developers new to Solana
- Auditors learning Solana-specific vulnerabilities
- Security researchers exploring the ecosystem
- Teams building secure DeFi protocols
Learn from history - these vulnerabilities have cost the ecosystem hundreds of millions.
| Protocol | Loss | Vulnerability | Our Module |
|---|---|---|---|
| Wormhole | $320M | Signature verification bypass | Signer Authorization |
| Cashio | $50M | Infinite mint via reinit | Reinitialization |
| Mango Markets | $114M | Oracle manipulation | Account Data Matching |
| Slope Wallet | $8M | Key exposure | Owner Check |
| Crema Finance | $8.8M | Flash loan + oracle | Integer Overflow |
| # | Vulnerability | Impact | Difficulty | Real Exploits |
|---|---|---|---|---|
| 1 | Signer Authorization | 🔴 Critical | 🟢 Beginner | Wormhole |
| 2 | Owner Check | 🟠 High | 🟢 Beginner | Multiple |
| 3 | Account Data Matching | 🟠 High | 🟡 Intermediate | Mango |
| 4 | Type Cosplay | 🔴 Critical | 🟡 Intermediate | Saber |
| 5 | Arbitrary CPI | 🔴 Critical | 🔴 Advanced | Multiple DeFi |
| 6 | Integer Overflow | 🔴 Critical | 🟡 Intermediate | Crema |
| 7 | Reinitialization | 🔴 Critical | 🟡 Intermediate | Cashio |
| 8 | PDA Seed Collision | 🟠 High | 🟡 Intermediate | Various |
| 9 | Closing Accounts | 🟠 High | 🟡 Intermediate | Various |
sequenceDiagram
participant Attacker
participant Program
participant Vault
Note over Attacker,Vault: Vulnerable: AccountInfo instead of Signer
Attacker->>Program: withdraw(victim_pubkey, amount)
Note right of Attacker: Passes pubkey without signing
Program->>Program: ❌ No signature check!
Program->>Vault: Transfer funds
Vault-->>Attacker: 💰 Funds stolen
sequenceDiagram
participant Protocol
participant Attacker
participant Config
Protocol->>Config: initialize(admin=PROTOCOL)
Note over Config: Admin set to protocol
Attacker->>Config: initialize(admin=ATTACKER)
Note over Config: ❌ No init check!
Note over Config: Admin overwritten!
Attacker->>Config: withdraw_treasury()
Config-->>Attacker: 💰 All funds
graph LR
A[Attacker Account<br/>Discriminator: User] -->|Passed as| B[Program]
C[Victim Account<br/>Discriminator: User] -->|Also valid| B
B -->|Same bytes, different meaning| D[❌ Wrong interpretation]
D --> E[💰 Funds stolen]
Location: programs/signer-authorization/
Missing or improper verification that an account has actually signed the transaction.
// ❌ INSECURE: No signer verification
pub authority: AccountInfo<'info>,
// ✅ SECURE: Anchor enforces signature
pub authority: Signer<'info>,The attacker bypassed signature verification on the guardian set, allowing them to mint wrapped ETH without valid signatures from the bridge guardians.
- Use
Signer<'info>for all authority accounts - Add
#[account(signer)]constraint when using AccountInfo - Manually check
authority.is_signer
Location: programs/owner-check/
Failing to verify that an account is owned by the expected program.
// ❌ INSECURE: Accepts any account
pub token_account: AccountInfo<'info>,
// ✅ SECURE: Verifies Token Program ownership
pub token_account: Account<'info, TokenAccount>,- Use Anchor's
Account<'info, T>wrapper - Manually verify with
constraint = account.owner == expected_program.key() - Never trust raw AccountInfo for data
Location: programs/account-data-matching/
Not validating that account relationships match expected constraints.
// ❌ INSECURE: No relationship verification
pub vault: Account<'info, Vault>,
pub authority: AccountInfo<'info>,
// ✅ SECURE: Enforces data matching
#[account(has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,- Use
has_oneconstraint for all account relationships - Add explicit
constraintchecks when needed - Verify account derivation with seeds
Location: programs/type-cosplay/
Different account types with identical data layouts can be confused.
// ❌ INSECURE: Manual deserialization without type checking
let data = User::try_from_slice(&account.data.borrow())?;
// ✅ SECURE: Anchor's discriminator prevents confusion
#[account]
pub struct User { ... } // Anchor adds 8-byte discriminator- Always use
#[account]for Anchor discriminators - Use
Account<'info, T>instead of raw AccountInfo - Add explicit type validation for dynamic account handling
Location: programs/arbitrary-cpi/
Allowing arbitrary programs to be invoked without verification.
// ❌ INSECURE: Any program accepted
pub token_program: AccountInfo<'info>,
// ✅ SECURE: Only Token Program allowed
pub token_program: Program<'info, Token>,- Use
Program<'info, T>with interface constraints - Whitelist allowed programs by ID
- Verify program IDs before CPI
Location: programs/integer-overflow/
Arithmetic operations that exceed type bounds cause unexpected behavior.
// ❌ INSECURE: Can underflow
vault.balance = vault.balance - amount;
// ✅ SECURE: Returns error on underflow
vault.balance = vault.balance
.checked_sub(amount)
.ok_or(ErrorCode::Underflow)?;- Use
checked_*methods for all arithmetic - Upcast to u128 for intermediate multiplication
- Enable
overflow-checks = truein release profile
Location: programs/reinitialization/
Allowing initialization functions to be called multiple times.
// ❌ INSECURE: Can be called multiple times
#[account(mut)]
pub config: Account<'info, Config>,
// ✅ SECURE: Anchor's init ensures first-time only
#[account(init, payer = admin, space = 8 + Config::INIT_SPACE)]
pub config: Account<'info, Config>,Attackers reinitialized the collateral account to mint infinite CASH tokens, crashing the stablecoin to zero.
- Always use Anchor's
initconstraint - Or check
is_initializedflag BEFORE any state changes - Use two-step admin transfer pattern
Location: programs/pda-seed-collision/
Poorly designed PDA seeds cause different logical entities to derive the same address.
// ❌ INSECURE: Missing unique identifier
seeds = [b"vault", user.key().as_ref()] // All tokens same PDA!
// ✅ SECURE: Include all unique identifiers
seeds = [b"vault", user.key().as_ref(), token_mint.key().as_ref()]- String concatenation collision: "AB" + "C" == "A" + "BC"
- Sequential IDs enable front-running
- Missing identifiers cause overwrites
- Include ALL unique identifiers in seeds
- Use fixed-size pubkeys instead of strings
- Add random nonce for unpredictable PDAs
- Canonical ordering for symmetric relationships
Location: programs/closing-accounts/
Improper account closing leads to fund loss or revival attacks.
// ❌ INSECURE: Manual close without zeroing
**vault.lamports.borrow_mut() = 0; // Data still readable!
// ✅ SECURE: Anchor's close zeros data
#[account(mut, close = authority)]
pub vault: Account<'info, Vault>,- Rent Extraction: Drain lamports, account persists briefly
- Revival Attack: Refund lamports in same transaction
- Force Defund: Anyone closes unprotected accounts
- Always use Anchor's
closeconstraint - Check balance == 0 before closing
- Track closure timestamp for revival detection
Test your skills with our Capture The Flag challenges!
challenges/
├── challenge-1-insecure-vault/ 🟢 Easy (100 pts)
├── challenge-2-token-drain/ 🟡 Medium (250 pts)
├── challenge-3-admin-takeover/ 🟡 Medium (250 pts)
├── challenge-4-flash-loan/ 🔴 Hard (500 pts)
└── challenge-5-oracle/ 🔴 Hard (500 pts)
See challenges/README.md for rules and instructions.
- Rust (1.70+)
- Solana CLI (1.17+)
- Anchor (0.31+)
- Node.js (18+)
# Clone the repository
git clone https://github.com/YOUR_USERNAME/solsec-workshop.git
cd solsec-workshop
# Install dependencies
npm install
# Build programs
anchor build
# Run tests
anchor test# Run specific vulnerability test
anchor test -- --grep "Signer Authorization"
# Run with verbose output
anchor test -- --reporter specsolsec-workshop/
├── programs/ # Vulnerable + secure programs
│ ├── signer-authorization/
│ ├── owner-check/
│ ├── account-data-matching/
│ ├── type-cosplay/
│ ├── arbitrary-cpi/
│ ├── integer-overflow/
│ ├── reinitialization/ # NEW
│ ├── pda-seed-collision/ # NEW
│ └── closing-accounts/ # NEW
├── tests/ # Educational test suites
├── challenges/ # CTF challenges
├── docs/
│ ├── DEEP_DIVE.md # Comprehensive analysis
│ └── AUDIT_CHECKLIST.md # Professional audit checklist
├── Anchor.toml
└── README.md
We provide a comprehensive audit checklist covering:
- ✅ Account Validation (Signer, Owner, Relationships)
- ✅ PDA Security (Seeds, Collisions)
- ✅ Arithmetic Safety (Overflow, Precision)
- ✅ Access Control (Init, Admin, Roles)
- ✅ CPI Security (Program verification)
- ✅ Account Lifecycle (Closing, Revival)
- ✅ Token Security (Transfers, Validation)
- ✅ Economic Security (Oracles, Flash Loans)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add your vulnerability example with tests
- Submit a pull request
This repository contains intentionally vulnerable code for educational purposes only. DO NOT deploy any insecure code to mainnet. The vulnerabilities demonstrated here have been exploited in real-world attacks causing hundreds of millions in losses.
MIT License - See LICENSE for details.
Built with ❤️ for the Solana security community
Vulnerabilities •
CTF Challenges •
Audit Checklist •
Deep Dive