Account Alias Attack (Role Confusion)
Detects when the same account is passed in multiple positions with incompatible role requirements, enabling an attacker to bypass security checks through role confusion.
Account Alias Attack (Role Confusion)
Overview
Remediation Guide: How to Fix Account Alias Attacks
The account alias attack detector identifies Solana programs where the same account variable is used in multiple conflicting roles within the same instruction. In Solana, accounts are passed as a flat array; the program is responsible for ensuring that accounts fulfilling different semantic roles are distinct. If a program does not verify that, for example, the fee payer and the authority are different accounts, an attacker can pass the same account in both positions.
Sigvex analyzes the control-flow graph to identify accounts assigned multiple incompatible roles across all basic blocks. The detector tracks role assignments — payer (from lamport transfers), owner (from ownership checks), authority (from authority-related syscalls), signer (from signer checks), and writable (from writability checks) — and reports any account variable appearing in roles that conflict by definition.
Conflicting role pairs: Payer↔Authority, Owner↔Authority, Payer↔Owner. Non-conflicting pairs (Signer+Writable, Payer+Signer) are permitted and not flagged. CWE mapping: CWE-269 (Improper Privilege Management).
Why This Is an Issue
Account aliasing allows an attacker to collapse security boundaries by using one account to satisfy multiple role constraints simultaneously in ways that violate protocol invariants. For example:
- Payer = Authority: An attacker passes their own account as both the transaction payer and the authority. If the protocol expects the authority to be a program-controlled multisig, the attacker gains admin privileges.
- Owner = Authority: When the same account is checked as an owner and used as an authority, an attacker controlling an owned account can elevate their role.
- Source = Destination: Passing the same token account as both source and destination in a transfer can bypass balance checks and create apparent “infinite” funds in some protocol designs.
Anchor’s typed account system (#[account(...)]) with explicit constraint annotations prevents this at the framework level, but native programs and Anchor programs using untyped accounts in remaining_accounts remain vulnerable.
How to Resolve
// Before: Vulnerable — no check that payer and authority are distinct
pub fn admin_operation(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let payer = &accounts[0];
let authority = &accounts[1]; // Could be same account as payer!
let treasury = &accounts[2];
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// If authority is the same as payer, attacker bypasses the intent
// of having a separate authority account
**treasury.lamports.borrow_mut() -= amount;
**payer.lamports.borrow_mut() += amount;
Ok(())
}
// After: Explicitly verify account distinctness
pub fn admin_operation(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let payer = &accounts[0];
let authority = &accounts[1];
let treasury = &accounts[2];
// Verify accounts are distinct where required
if payer.key == authority.key {
return Err(ProgramError::InvalidArgument);
}
if payer.key == treasury.key || authority.key == treasury.key {
return Err(ProgramError::InvalidArgument);
}
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**treasury.lamports.borrow_mut() -= amount;
**payer.lamports.borrow_mut() += amount;
Ok(())
}
For Anchor:
// Anchor: use constraint to enforce account distinctness
#[derive(Accounts)]
pub struct AdminOperation<'info> {
#[account(mut)]
pub payer: Signer<'info>,
// Explicitly constrain that authority is not the same as payer
#[account(constraint = authority.key() != payer.key() @ CustomError::SameAccount)]
pub authority: Signer<'info>,
#[account(mut, constraint = treasury.key() != payer.key() && treasury.key() != authority.key())]
pub treasury: Account<'info, Treasury>,
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
// Vulnerable: attacker can pass same account as both vault owner and fee recipient
pub fn claim_fees(accounts: &[AccountInfo], fee_amount: u64) -> ProgramResult {
let vault_owner = &accounts[0];
let fee_recipient = &accounts[1];
let vault = &accounts[2];
// Only checks that vault_owner is a signer — not that vault_owner != fee_recipient
if !vault_owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// VULNERABLE: attacker passes vault_owner == fee_recipient
// Protocol intends fee_recipient to be a treasury, not the user themselves
**vault.lamports.borrow_mut() -= fee_amount;
**fee_recipient.lamports.borrow_mut() += fee_amount;
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
// Fixed treasury address — enforces fee destination
const PROTOCOL_TREASURY: Pubkey = /* your treasury pubkey */;
pub fn claim_fees(accounts: &[AccountInfo], fee_amount: u64) -> ProgramResult {
let vault_owner = &accounts[0];
let fee_recipient = &accounts[1];
let vault = &accounts[2];
// Check: vault owner must sign
if !vault_owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Check: accounts must be distinct (prevent aliasing)
if vault_owner.key == fee_recipient.key {
return Err(ProgramError::InvalidArgument);
}
// Check: fee recipient must be the protocol treasury
if fee_recipient.key != &PROTOCOL_TREASURY {
return Err(ProgramError::InvalidArgument);
}
**vault.lamports.borrow_mut() -= fee_amount;
**fee_recipient.lamports.borrow_mut() += fee_amount;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "account-alias-attack",
"severity": "high",
"confidence": 0.78,
"description": "Account v1 is used in conflicting roles: Payer (block 0, stmt 3) and Authority (block 0, stmt 5). An attacker could pass the same account in multiple positions to bypass security checks, enabling privilege escalation or unauthorized operations.",
"location": { "function": "claim_fees", "offset": 3 }
}
Detection Methodology
The detector analyzes all basic blocks in the function’s CFG, building a role map for each account variable:
- Role identification: For each statement, classifies the account’s role:
TransferLamportsfrom-account → Payer roleCheckOwneraccount → Owner role- Authority-related syscalls (mint, burn, authority keywords in syscall names) → Authority role
CheckSigner→ Signer roleCheckWritable→ Writable role
- Cross-block accumulation: Roles are accumulated across all blocks, since aliasing across block boundaries (e.g., the account is used as a payer in block 0 and as authority in block 1) is equally dangerous.
- Conflict detection: All pairs of roles for each account are checked against the conflict matrix. Conflicting pairs generate a finding with confidence 0.78.
Context modifiers:
- Anchor programs with discriminator validation: confidence reduced by 80% (Account
type system prevents aliasing) - Anchor programs without discriminator validation: confidence reduced by 60%
- PDA-derived accounts: confidence reduced by 50% (deterministic addresses reduce aliasing risk)
- Read-only functions: confidence reduced by 60%
Limitations
False positives:
- Protocols where the same account legitimately holds multiple roles (e.g., an admin multisig that is also the payer for a specific operation) may be flagged.
- Anchor programs using
Account<'info, T>with explicitconstraintannotations enforcing account distinctness may be flagged before the context reducer applies.
False negatives:
- Aliasing at the public key level (two different variables pointing to the same account via separate
AccountInforeferences) requires constant propagation of pubkey values to detect, which is not fully supported. - Conditional aliasing (attacker passes same account only when a specific condition is true) may not be detected without path-sensitive analysis.
Related Detectors
- Duplicate Mutable Accounts — detects the specific case of the same mutable account passed multiple times
- Missing Owner Check — often combined with aliasing to bypass role separation
- Missing Signer Check — missing signer validation amplifies aliasing attacks