Remediating Account Alias Attacks
How to prevent account role confusion by explicitly validating that accounts in distinct roles have distinct addresses.
Remediating Account Alias Attacks
Overview
Related Detector: Account Alias Attack
Account aliasing occurs when an attacker passes the same Solana account in multiple positions that the program treats as having distinct roles. The fix is to add explicit key-equality checks between accounts that must be distinct, or to use Anchor’s constraint system to enforce distinctness at the framework level.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
// Vulnerable: no check that 'from' and 'to' are different accounts
pub fn transfer_between_vaults(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let source_vault = &accounts[0];
let dest_vault = &accounts[1];
let owner = &accounts[2];
if !owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// VULNERABLE: attacker passes source_vault == dest_vault
// Balance decreases and then increases on same account — net effect is zero
// but the program may have logged a transfer or emitted events
**source_vault.lamports.borrow_mut() -= amount;
**dest_vault.lamports.borrow_mut() += amount;
Ok(())
}
After (Fixed)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn transfer_between_vaults(
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let source_vault = &accounts[0];
let dest_vault = &accounts[1];
let owner = &accounts[2];
// Check: accounts that should be distinct must be distinct
if source_vault.key == dest_vault.key {
msg!("Source and destination vault must be different accounts");
return Err(ProgramError::InvalidArgument);
}
if owner.key == source_vault.key || owner.key == dest_vault.key {
msg!("Owner must be distinct from vault accounts");
return Err(ProgramError::InvalidArgument);
}
if !owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**source_vault.lamports.borrow_mut() -= amount;
**dest_vault.lamports.borrow_mut() += amount;
Ok(())
}
Alternative Mitigations
Anchor Constraint-Based Distinctness
In Anchor programs, use the constraint attribute to enforce account distinctness declaratively:
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct TransferBetweenVaults<'info> {
#[account(
mut,
// Explicitly require source and destination are different accounts
constraint = source_vault.key() != dest_vault.key() @ CustomError::SameAccount,
// Ownership check
constraint = source_vault.owner_pubkey == authority.key() @ CustomError::Unauthorized,
)]
pub source_vault: Account<'info, Vault>,
#[account(
mut,
constraint = dest_vault.key() != authority.key() @ CustomError::InvalidDestination,
)]
pub dest_vault: Account<'info, Vault>,
pub authority: Signer<'info>,
}
Anchor’s constraint system evaluates these checks at account deserialization time, before any instruction logic runs. This provides early rejection of invalid account combinations.
Use PDAs to Enforce Address Uniqueness
When accounts serve protocol-controlled roles (treasury, fee collector, etc.), derive them as PDAs from unique seeds. Since PDA addresses are deterministic, the same account cannot legitimately appear in two positions with different protocol-assigned roles:
// Treasury PDA — only one address is valid for the treasury role
let (treasury_pda, _) = Pubkey::find_program_address(
&[b"treasury", program_id.as_ref()],
&program_id,
);
// In account validation:
require!(
treasury_account.key == &treasury_pda,
CustomError::InvalidTreasury
);
Common Mistakes
Mistake: Only Checking Signers, Not Distinctness
// WRONG: verifies authority is a signer but not that authority != recipient
pub fn admin_withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let authority = &accounts[0];
let recipient = &accounts[1];
require!(authority.is_signer, ProgramError::MissingRequiredSignature);
// No check that authority != recipient
// An attacker passes authority == recipient to receive admin-controlled funds
transfer_to(recipient, amount)?;
Ok(())
}
Mistake: Checking Distinctness in the Wrong Order
// WRONG: performs the operation before the distinctness check
pub fn transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let from = &accounts[0];
let to = &accounts[1];
// Transfer happens first
**from.lamports.borrow_mut() -= amount;
**to.lamports.borrow_mut() += amount;
// Distinctness check too late — state already modified
require!(from.key != to.key, ProgramError::InvalidArgument);
Ok(())
}
Always validate all preconditions — including account distinctness — before any state mutation.
Mistake: Anchor mut Without Distinctness Constraints
// INCOMPLETE: both accounts are validated as Vault type but not as distinct
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(mut)] // Missing: constraint = source.key() != destination.key()
pub source: Account<'info, Vault>,
#[account(mut)]
pub destination: Account<'info, Vault>,
pub authority: Signer<'info>,
}
Anchor’s type system ensures both are Vault accounts owned by your program, but it does not prevent the same vault from being passed in both positions. Add an explicit constraint.