Account Type Confusion Exploit Generator
Sigvex exploit generator that validates account type confusion vulnerabilities in Solana programs by simulating an attack that passes a wrong account type to an instruction, bypassing type-based access controls.
Account Type Confusion Exploit Generator
Overview
The account type confusion exploit generator validates findings from the account-type-confusion and token-account-ownership detectors by simulating an attack that passes a wrong account type to an instruction. If the program processes the instruction without validating the account’s discriminator or type, the vulnerability is confirmed with high confidence.
Solana programs store arbitrary bytes in accounts. An account intended to hold a Vault struct might be confused with an account holding a LoanRecord struct if the program does not verify the account’s type discriminator before deserializing. In Anchor, each account type is prefixed with a unique 8-byte discriminator. If this discriminator is not checked (e.g., when using AccountInfo instead of typed Account<'info, Vault>), an attacker can pass an account of the wrong type to exploit the type confusion.
Severity score: 80/100 (High).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Token account ownership bypass:
- A program’s
withdrawinstruction accepts auser_token_accountand transfers tokens fromvault_token_accountto it. - The program checks
user_token_account.owner == program_id(a custom check) but does not verify the account is a valid SPL token account. - An attacker creates a fake account that passes the ownership check but is not a real token account.
- The program attempts to transfer tokens to the fake account.
- Depending on the deserialization, the transfer may succeed with incorrect account data, corrupting state.
Account discriminator bypass:
- A governance program has two account types:
ProposalAccountandVoteAccount. - An instruction for creating a proposal accepts a
ProposalAccountbut usesAccountInfoinstead ofAccount<'info, ProposalAccount>. - The program does not check the discriminator.
- An attacker passes a
VoteAccountwhere aProposalAccountis expected. - The deserialized data from
VoteAccountis interpreted asProposalAccountfields, corrupting governance state.
Exploit Mechanics
The engine maps account-type-confusion and token-account-ownership detector findings to the account confusion exploit pattern (severity score 80/100).
Strategy: Create an account of a different type (e.g., LoanRecord) that shares field alignment with the expected type (Vault), then pass it as the vault account in an instruction that uses AccountInfo rather than the typed Account<'info, Vault>.
Simulation steps:
- The engine identifies the vulnerable instruction from the finding location — specifically, instructions using
AccountInfofor accounts that should be typed (indicated by/// CHECK:comment in source or absence of discriminator check in bytecode). - A crafted account is initialized at a valid address with: correct
owner(the program ID) so ownership passes, but account data bytes that represent a different struct type. The discriminator (first 8 bytes) is set to that of an attacker-controlled type. - The exploit transaction passes this crafted account to the instruction.
- If the program deserializes without validating the discriminator (e.g.,
T::try_from_slice(&data)on raw bytes), it will interpret fields from the wrong struct, potentially reading an attacker-controlledbalancefield. - If execution succeeds without an
AccountDidNotDeserializeor discriminator mismatch error, the vulnerability is confirmed as likely exploitable with confidence 0.85. - Evidence recorded:
injected_account_type,expected_account_type,discriminator_mismatch,impact_description.
Why this is exploitable:
Anchor’s Account<'info, T> type automatically checks the first 8 bytes against T::discriminator(). When a program uses AccountInfo<'info> instead, this check is bypassed entirely. The attacker’s crafted account data is deserialized directly, allowing them to supply any values for fields the program will then act upon as trusted data.
// VULNERABLE: No discriminator check — uses AccountInfo
use anchor_lang::prelude::*;
#[program]
mod vulnerable_protocol {
pub fn process_vault(ctx: Context<ProcessVault>) -> Result<()> {
// AccountInfo bypasses Anchor's discriminator check!
let vault_data = ctx.accounts.vault.try_borrow_data()?;
// Deserializes raw bytes — attacker controls account type
let vault: Vault = Vault::try_from_slice(&vault_data)?;
// vault fields could be from a completely different account type
emit!(VaultProcessed { balance: vault.balance });
Ok(())
}
}
#[derive(Accounts)]
pub struct ProcessVault<'info> {
/// CHECK: No type checking! Vulnerable!
pub vault: AccountInfo<'info>,
pub authority: Signer<'info>,
}
// ATTACK: Pass a LoanRecord account where Vault is expected
// LoanRecord.amount field aligns with Vault.balance in memory
// Program reads attacker-controlled loan_amount as vault_balance
// SECURE: Use typed Account<'info, T> for automatic discriminator check
#[derive(Accounts)]
pub struct ProcessVaultSafe<'info> {
// Anchor automatically verifies:
// 1. Account discriminator (first 8 bytes match Vault::discriminator())
// 2. Account is owned by this program
// 3. Data length is sufficient for Vault struct
#[account(has_one = authority)]
pub vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
// Verify token account ownership manually when using AccountInfo
fn verify_token_account(
token_account: &AccountInfo,
expected_owner: &Pubkey,
expected_mint: &Pubkey,
) -> Result<()> {
// Verify owned by SPL Token program
if token_account.owner != &spl_token::id() {
return Err(ErrorCode::InvalidTokenAccount.into());
}
// Deserialize and check fields
let token_data = spl_token::state::Account::unpack(&token_account.data.borrow())?;
if &token_data.owner != expected_owner {
return Err(ErrorCode::InvalidTokenAccountOwner.into());
}
if &token_data.mint != expected_mint {
return Err(ErrorCode::InvalidTokenMint.into());
}
Ok(())
}
Remediation
- Detector: Account Type Confusion Detector
- Remediation Guide: Account Type Confusion Remediation
Use Anchor’s typed account system to prevent all discriminator bypass attacks:
// Always prefer: Account<'info, YourType> over AccountInfo<'info>
// Anchor automatically:
// 1. Checks the 8-byte discriminator matches YourType
// 2. Verifies program ownership
// 3. Deserializes with correct type
// For SPL token accounts, use the anchor_spl types:
use anchor_spl::token::TokenAccount;
pub user_token_account: Account<'info, TokenAccount>,
// Additional constraints for token accounts:
#[account(
token::mint = expected_mint,
token::authority = user,
)]
pub user_token_account: Account<'info, TokenAccount>,
When AccountInfo is unavoidable (e.g., for dynamic account dispatch), always manually verify:
- Account owner matches expected program ID.
- Account data starts with the expected discriminator (first 8 bytes).
- Account data length is at least the expected struct size.