Severity: CRITICAL
This example demonstrates the Missing Discriminator Check vulnerability, where a program manually deserializes account data without verifying the 8-byte discriminator that identifies the account type.
Anchor uses an 8-byte discriminator (derived from SHA256("account:TypeName")[0..8]) as the first 8 bytes of every account to identify its type. When you use AccountInfo and manually deserialize data, you bypass this check.
If two account types have the same data layout (after the discriminator), an attacker can pass the wrong account type and your program will misinterpret the data!
This vulnerability can lead to:
- Type confusion - Reading Config.setting as Vault.balance
- Privilege escalation - Using admin accounts as user accounts
- Fund theft - Interpreting arbitrary data as withdrawal amounts
- State corruption - Writing to wrong account types
- What discriminators are and why they matter
- How Anchor uses discriminators for type safety
- The danger of manual deserialization with
AccountInfo - How
Account<T>prevents type confusion attacks
This program has two account types with identical data layouts:
- Vault - Stores user funds (owner, balance, bump)
- Config - Stores settings (admin, setting_value, bump)
And four instructions:
initialize_vault- Creates a Vault accountinitialize_config- Creates a Config accountwithdraw_vulnerable- Deserializes without discriminator check (VULNERABLE)withdraw_secure- UsesAccount<Vault>with automatic check (SECURE)
Located in: src/instructions/withdraw_vulnerable.rs
#[derive(Accounts)]
pub struct WithdrawVulnerable<'info> {
/// ⚠️ Using AccountInfo - no type safety!
#[account(mut)]
pub vault: AccountInfo<'info>,
pub owner: Signer<'info>,
}
pub fn handler(ctx: Context<WithdrawVulnerable>, amount: u64) -> Result<()> {
let data = vault_info.try_borrow_data()?;
// ⚠️ VULNERABLE: Skip discriminator without checking it!
let owner = Pubkey::try_from(&data[8..40]).unwrap();
let balance = u64::from_le_bytes(data[40..48].try_into().unwrap());
// If attacker passes a Config account:
// - owner reads Config.admin
// - balance reads Config.setting_value (could be huge!)
require!(balance >= amount, InsufficientFunds);
// Attacker withdraws based on setting_value, not actual balance!
}Both account types have the same layout after discriminator:
Vault Account:
┌──────────────┬─────────┬─────────┬──────┐
│ discriminator│ owner │ balance │ bump │
│ 8 bytes │32 bytes │ 8 bytes │1 byte│
└──────────────┴─────────┴─────────┴──────┘
Config Account:
┌──────────────┬─────────┬──────────────┬──────┐
│ discriminator│ admin │setting_value │ bump │
│ 8 bytes │32 bytes │ 8 bytes │1 byte│
└──────────────┴─────────┴──────────────┴──────┘
Without discriminator check, Config looks like Vault!
Step-by-step exploit:
ATTACKER CREATES CONFIG ACCOUNT
┌─────────────────────────────────────────┐
│ Config { │
│ admin: attacker_pubkey, │
│ setting_value: 1_000_000_000, // HUGE!│
│ bump: 255, │
│ } │
└─────────────────────────────────────────┘
│
│ Pass as "vault" parameter
▼
YOUR PROGRAM (withdraw_vulnerable)
┌─────────────────────────────────────────┐
│ 1. Read data[8..40] as owner │
│ → Gets Config.admin (attacker) ✓ │
│ 2. Read data[40..48] as balance │
│ → Gets Config.setting_value (1B!) 🚨│
│ 3. Check owner == signer ✓ │
│ 4. Check balance >= amount ✓ │
│ 5. Withdraw 1B based on fake balance! │
└─────────────────────────────────────────┘
// Attacker creates a Config with huge setting_value
await program.methods
.initializeConfig(new BN(1_000_000_000)) // 1 billion!
.accounts({
config: configPDA,
admin: attacker.publicKey,
})
.rpc();
// Pass Config account as "vault" to vulnerable instruction
await program.methods
.withdrawVulnerable(new BN(1_000_000_000))
.accounts({
vault: configPDA, // Config, not Vault!
owner: attacker.publicKey,
recipient: attacker.publicKey,
})
.rpc();
// Program reads Config.setting_value as Vault.balance!Located in: src/instructions/withdraw_secure.rs
#[derive(Accounts)]
pub struct WithdrawSecure<'info> {
/// ✅ SECURE: Account<Vault> checks discriminator automatically
#[account(
mut,
seeds = [b"vault", vault.owner.as_ref()],
bump = vault.bump,
has_one = owner
)]
pub vault: Account<'info, Vault>,
pub owner: Signer<'info>,
}
pub fn handler(ctx: Context<WithdrawSecure>, amount: u64) -> Result<()> {
let vault = &mut ctx.accounts.vault;
// ✅ vault.balance is guaranteed to be from a real Vault
require!(vault.balance >= amount, InsufficientFunds);
vault.balance -= amount;
Ok(())
}When you use Account<'info, Vault>, Anchor automatically verifies:
| Check | Description |
|---|---|
| Owner | Account is owned by this program |
| Discriminator | First 8 bytes match SHA256("account:Vault")[0..8] |
| Deserialization | Data correctly deserializes to Vault struct |
ATTACKER'S CONFIG ACCOUNT
┌─────────────────────────────────────────┐
│ discriminator: [Config's discriminator] │
│ admin: attacker_pubkey │
│ setting_value: 1_000_000_000 │
└─────────────────────────────────────────┘
│
│ Pass as "vault" parameter
▼
YOUR PROGRAM (withdraw_secure)
┌─────────────────────────────────────────┐
│ Anchor checks: │
│ discriminator == Vault discriminator? │
│ │
│ Config disc != Vault disc │
│ │
│ ❌ REJECTED! AccountDidNotDeserialize │
└─────────────────────────────────────────┘
Anchor generates discriminators using:
// For an account named "Vault"
let discriminator = &Sha256::hash(b"account:Vault")[0..8];Each account type has a unique discriminator:
Vault→[211, 8, 232, 43, 2, 152, 117, 119]Config→[different 8 bytes]
- Use
AccountInfofor accounts that need type validation - Manually deserialize without checking discriminator
- Skip the first 8 bytes assuming they're "just metadata"
- Trust that callers pass the correct account type
- Use
Account<'info, T>for typed accounts - Let Anchor handle discriminator validation
- Use
#[account]attribute for all account structs - Manually check discriminator if you must use
AccountInfo
If you must use AccountInfo, check the discriminator manually:
use anchor_lang::Discriminator;
let data = account_info.try_borrow_data()?;
let discriminator = &data[0..8];
require!(
discriminator == Vault::DISCRIMINATOR,
ErrorCode::InvalidDiscriminator
);cd 6-missing-discriminator
anchor buildRemember: The discriminator is your program's first line of defense against type confusion. Always use Account<T> to let Anchor protect you automatically!