Skip to content

Latest commit

 

History

History

README.md

Arithmetic Overflow/Underflow - Security Example

🎯 Vulnerability Overview

Severity: 🔴 CRITICAL

This example demonstrates one of the most common and dangerous vulnerabilities in smart contracts: Arithmetic Overflow and Underflow.

The Problem

When performing math operations on integers, the result can exceed the maximum value (overflow) or go below zero (underflow). In Rust release builds without proper checks, these operations wrap around silently instead of causing errors!

Example:

  • Overflow: 255u8 + 1 = 0 (wraps around to minimum)
  • Underflow: 0u64 - 1 = 18,446,744,073,709,551,615 (wraps to maximum)

Real-World Impact

This vulnerability has led to some of the largest exploits in blockchain history:

  • Infinite money exploits - Users creating unlimited tokens from nothing
  • Protocol drainage - Attackers withdrawing more than deposited
  • Balance corruption - Account balances becoming invalid

Famous Examples:

  • BeautyChain (BEC): $900M market cap wiped out due to overflow bug
  • Various DeFi protocols drained by underflow exploits

📚 What You'll Learn

  1. ✅ Why unchecked arithmetic is dangerous
  2. ✅ How integer overflow/underflow works
  3. ✅ The difference between debug and release builds
  4. ✅ How to use checked arithmetic in Rust
  5. ✅ Multiple approaches to safe math operations

🏗️ Program Structure

This program contains a simple token system with:

  1. initialize - Creates a token account with initial balance
  2. deposit - Adds tokens (uses checked math)
  3. withdraw_vulnerable ⚠️ - Withdraws with unchecked math (VULNERABLE)
  4. withdraw_secure ✅ - Withdraws with checked math (SECURE)

⚠️ The Vulnerability: Unchecked Arithmetic

Vulnerable Code

Located in: src/instructions/withdraw_vulnerable.rs

pub fn handler(ctx: Context<WithdrawVulnerable>, amount: u64) -> Result<()> {
    let token_account = &mut ctx.accounts.token_account;

    // ⚠️ VULNERABLE: Using unchecked subtraction
    token_account.balance = token_account.balance - amount;

    Ok(())
}

Why This Is Dangerous

The - operator in Rust:

  • Debug builds: Panics on overflow/underflow ✓
  • Release builds: Silently wraps around ❌

Production Solana programs run in release mode, so:

  • 0 - 1 = 18,446,744,073,709,551,615 (u64::MAX)
  • No error, no panic - just infinite money!

The Attack

Step-by-step exploit:

  1. Alice initializes a token account with 0 balance
  2. Alice calls withdraw_vulnerable(1)
    • Current balance: 0
    • Tries to subtract: 0 - 1
    • Result: 18,446,744,073,709,551,615 (u64::MAX)
  3. Alice now has 18.4 quintillion tokens! 💰💰💰

Attack Code Example

// Initialize with 0 balance
await program.methods
  .initialize(new BN(0))
  .accounts({
    tokenAccount: tokenAccountPDA,
    owner: attacker.publicKey,
  })
  .rpc();

// Exploit: Withdraw 1 token when balance is 0
await program.methods
  .withdrawVulnerable(new BN(1))
  .accounts({
    tokenAccount: tokenAccountPDA,
    owner: attacker.publicKey,
  })
  .rpc();

// Balance is now 18,446,744,073,709,551,615 (u64::MAX)
// Infinite money created from nothing! 🚨

✅ The Fix: Checked Arithmetic

Located in: src/instructions/withdraw_secure.rs

pub fn handler(ctx: Context<WithdrawSecure>, amount: u64) -> Result<()> {
    let token_account = &mut ctx.accounts.token_account;

    // ✅ SECURE: Using checked_sub()
    token_account.balance = token_account
        .balance
        .checked_sub(amount)
        .ok_or(ErrorCode::InsufficientBalance)?;

    Ok(())
}

How It Prevents The Attack

When using checked_sub():

  1. Attacker tries to withdraw 1 token with 0 balance
  2. 0.checked_sub(1) returns None (operation would underflow)
  3. .ok_or(ErrorCode::InsufficientBalance)? converts None to error
  4. Transaction FAILS - no underflow, no infinite tokens! ✅

Multiple Safe Approaches

1. Checked Operations (Recommended)

// Returns Option<T>, None if overflow/underflow
balance.checked_sub(amount).ok_or(ErrorCode::InsufficientBalance)?
balance.checked_add(amount).ok_or(ErrorCode::Overflow)?
balance.checked_mul(multiplier).ok_or(ErrorCode::Overflow)?
balance.checked_div(divisor).ok_or(ErrorCode::DivisionError)?

2. Saturating Operations

// Clamps to min/max instead of wrapping
balance = balance.saturating_sub(amount);  // Returns 0 if would underflow
balance = balance.saturating_add(amount);  // Returns MAX if would overflow

3. Manual Validation

// Check before operation
require!(balance >= amount, ErrorCode::InsufficientBalance);
balance -= amount;

4. Overflow Checks in Cargo.toml (Defense in Depth)

[profile.release]
overflow-checks = true  # Makes release builds panic on overflow

🔍 Debug vs Release Builds

Understanding the difference is crucial:

Debug Builds (Development)

let x: u8 = 255;
let y = x + 1;  // PANICS! "arithmetic operation overflow"

Release Builds (Production)

let x: u8 = 255;
let y = x + 1;  // y = 0 (wraps around silently) 😱

Solana programs run in RELEASE mode, so you MUST use checked arithmetic!

🔑 Key Takeaways

❌ DON'T

  • Use +, -, *, / operators on user-controlled values
  • Assume Rust will prevent overflow in production
  • Rely on debug build behavior for safety
  • Use saturating_* without understanding implications

✅ DO

  • Use checked_add(), checked_sub(), checked_mul(), checked_div()
  • Add overflow-checks = true in Cargo.toml for defense in depth
  • Validate inputs before arithmetic operations
  • Test with boundary values (0, MAX, near-MAX)

📊 Comparison Table

Operation Behavior on Overflow Use Case
a + b Wraps in release ❌ Never use for money
a.checked_add(b) Returns None ✅ Financial operations
a.saturating_add(b) Clamps to MAX ⚠️ Specific use cases
a.wrapping_add(b) Always wraps ⚠️ When wrapping is intended

🚀 Building This Example

# Navigate to the program directory
cd 1-arithmetic-overflow

# Build the program
anchor build

📚 Additional Resources

⚖️ License

This code is for educational purposes only. The vulnerable code is intentionally insecure to demonstrate security concepts.


Remember: Always use checked arithmetic for financial operations. A single - instead of .checked_sub() can cost millions!