Account Type Confusion Remediation
Overview
Related Detector: Account Type Confusion
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 the account-validation framework’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 the account-validation framework'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.