Skip to main content
Sigvex

Token Balance Invariant Remediation

How to fix token operations that violate balance accounting invariants.

Token Balance Invariant Remediation

Overview

Related Detector: Token Balance Invariant

Balance invariant violations occur when token operations update account balances without corresponding supply updates, or vice versa. The fix ensures all mint/burn/transfer operations maintain the invariant sum(balances) == total_supply.

Before (Vulnerable)

// Mint without supply update -- creates phantom tokens
pub fn custom_mint(account: &mut Account, amount: u64) -> ProgramResult {
    account.amount = account.amount.checked_add(amount)?;
    // Missing: mint.supply update!
    Ok(())
}

After (Fixed)

pub fn custom_mint(mint: &mut Mint, account: &mut Account, amount: u64) -> ProgramResult {
    // Update both atomically
    account.amount = account.amount
        .checked_add(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;
    mint.supply = mint.supply
        .checked_add(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;
    Ok(())
}

Alternative Mitigations

1. Use SPL Token CPI instead of custom logic

The SPL Token program maintains invariants automatically. Prefer CPI over custom token logic:

let ix = spl_token::instruction::mint_to(
    &spl_token::id(), mint.key, destination.key, authority.key, &[], amount,
)?;
invoke(&ix, accounts)?;
// SPL Token updates both account.amount and mint.supply

2. Post-operation invariant check

Add an assertion after operations to verify the invariant holds:

fn verify_invariant(mint: &Mint, accounts: &[Account]) -> ProgramResult {
    let total_balances: u64 = accounts.iter()
        .map(|a| a.amount)
        .try_fold(0u64, |acc, x| acc.checked_add(x))
        .ok_or(ProgramError::ArithmeticOverflow)?;
    require!(total_balances == mint.supply, InvariantViolation);
    Ok(())
}

3. Event-based auditing

Emit events for all balance changes to enable off-chain invariant verification:

emit!(MintEvent { mint: mint.key(), amount, new_supply: mint.supply });

Common Mistakes

Mistake 1: Updating supply but not account balance

// WRONG: supply increases but no account receives the tokens
mint.supply += amount;
// Missing: account.amount += amount;

Mistake 2: Using wrapping arithmetic

// WRONG: wrapping_add silently overflows, breaking invariants
account.amount = account.amount.wrapping_add(amount);
// CORRECT: checked_add returns error on overflow
account.amount = account.amount.checked_add(amount)?;

Mistake 3: Asymmetric transfer

// WRONG: source decreases by different amount than destination increases
source.amount -= amount;
destination.amount += amount - fee; // Fee disappears from total!
// CORRECT: fee should go somewhere (fee account or burn)

References