Account Type Confusion Remediation
How to validate account discriminators and use Anchor's typed account system to prevent attackers from substituting accounts of the wrong type.
Account Type Confusion Remediation
Overview
Related Detector: Type Cosplay
Account type confusion — also called type cosplay — occurs when a Solana program deserializes account data without verifying that the account actually contains the expected struct type. In Anchor, every account type is prefixed with a unique 8-byte discriminator derived from the type name. If a program uses AccountInfo<'info> instead of Account<'info, T>, or calls try_from_slice directly on raw bytes, the discriminator check is bypassed. An attacker can then craft an account of a different type whose field layout overlaps with the expected type, causing the program to act on attacker-controlled field values.
The fix is to use Anchor’s typed account system wherever possible, and to manually validate the discriminator, program owner, and data length whenever AccountInfo is unavoidable.
Recommended Fix
Before (Vulnerable)
use anchor_lang::prelude::*;
#[program]
mod vulnerable_protocol {
pub fn process_vault(ctx: Context<ProcessVault>) -> Result<()> {
// VULNERABLE: AccountInfo bypasses Anchor's discriminator check entirely
let data = ctx.accounts.vault.try_borrow_data()?;
// Raw deserialization — no discriminator verification
let vault = Vault::try_from_slice(&data)?;
// vault.balance comes from attacker-controlled bytes
msg!("Vault balance: {}", vault.balance);
transfer_funds(&ctx, vault.balance)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct ProcessVault<'info> {
/// CHECK: No type validation — vulnerable to type confusion!
pub vault: AccountInfo<'info>,
pub authority: Signer<'info>,
}
After (Fixed)
use anchor_lang::prelude::*;
#[program]
mod secure_protocol {
pub fn process_vault(ctx: Context<ProcessVault>) -> Result<()> {
// FIXED: vault is typed — Anchor already verified discriminator,
// program ownership, and data length during account deserialization
let vault = &ctx.accounts.vault;
msg!("Vault balance: {}", vault.balance);
transfer_funds(&ctx, vault.balance)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct ProcessVault<'info> {
// Account<'info, Vault> automatically verifies:
// 1. First 8 bytes == sha256("account:Vault")[..8]
// 2. Account is owned by this program
// 3. Account data length >= size_of::<Vault>() + 8
#[account(has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
Switching from AccountInfo to Account<'info, Vault> eliminates the type confusion surface. Anchor validates all three required properties — discriminator, owner, and size — before the handler body executes.
Alternative Mitigations
1. SPL token accounts via anchor_spl typed wrappers
For SPL token accounts, use the provided typed wrappers with additional mint and authority constraints:
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
#[derive(Accounts)]
pub struct Withdraw<'info> {
// TokenAccount checks: owner == spl_token::id(), discriminator, data length
#[account(
mut,
token::mint = vault_mint, // Ensures correct mint
token::authority = user, // Ensures correct authority
)]
pub user_token_account: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = vault_mint,
)]
pub vault_token_account: Account<'info, TokenAccount>,
pub vault_mint: Account<'info, Mint>,
pub user: Signer<'info>,
pub token_program: Program<'info, Token>,
}
2. Manual discriminator check when AccountInfo is required
In some patterns — such as dynamic dispatch or generic account handling — AccountInfo cannot be avoided. Always perform these four checks manually:
use anchor_lang::prelude::*;
fn validate_and_deserialize_vault(
account: &AccountInfo,
program_id: &Pubkey,
) -> Result<Vault> {
// 1. Verify the account is owned by this program
require!(
account.owner == program_id,
ErrorCode::InvalidAccountOwner
);
let data = account.try_borrow_data()?;
// 2. Verify minimum data length (discriminator + struct)
const DISCRIMINATOR_LEN: usize = 8;
require!(
data.len() >= DISCRIMINATOR_LEN + std::mem::size_of::<Vault>(),
ErrorCode::AccountDataTooSmall
);
// 3. Verify the discriminator matches Vault
let expected = Vault::discriminator();
require!(
data[..DISCRIMINATOR_LEN] == expected,
ErrorCode::AccountDiscriminatorMismatch
);
// 4. Deserialize (skip discriminator bytes)
let vault = Vault::try_from_slice(&data[DISCRIMINATOR_LEN..])?;
Ok(vault)
}
3. Owner verification for accounts owned by external programs
When validating accounts owned by other programs (e.g., the System Program or the SPL Token program), check the owner field explicitly:
pub fn verify_system_account(account: &AccountInfo) -> Result<()> {
// System-owned accounts have no discriminator — check the owner
require!(
account.owner == &solana_program::system_program::id(),
ErrorCode::InvalidAccountOwner
);
Ok(())
}
Common Mistakes
Mistake 1: Using AccountInfo with a /// CHECK: comment but no actual validation
// WRONG: The comment acknowledges the risk but provides no protection
#[derive(Accounts)]
pub struct BadExample<'info> {
/// CHECK: safe because we check the owner
pub vault: AccountInfo<'info>, // But the owner check never happens in the handler!
}
Every /// CHECK: annotation must be paired with actual runtime validation in the handler or as an account constraint.
Mistake 2: Relying on key equality to distinguish account types
// WRONG: two different account types can exist at the same address over time
require!(
ctx.accounts.account.key() == expected_vault_key,
ErrorCode::InvalidAccount
);
// This does not verify the account's current type — only its address
Address equality is not type equality. Always check the discriminator.
Mistake 3: Calling try_from_slice on the full account data including the discriminator
// WRONG: Anchor accounts include an 8-byte discriminator prefix
let vault = Vault::try_from_slice(&data)?; // Panics or produces garbage data
// CORRECT: skip the first 8 bytes
let vault = Vault::try_from_slice(&data[8..])?;
If using raw Borsh deserialization on Anchor accounts, always skip the leading 8-byte discriminator.