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.
Recommended Fix
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)