Severity: 🔴 CRITICAL
This example demonstrates one of the most common and dangerous vulnerabilities in smart contracts: Arithmetic Overflow and Underflow.
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)
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
- ✅ Why unchecked arithmetic is dangerous
- ✅ How integer overflow/underflow works
- ✅ The difference between debug and release builds
- ✅ How to use checked arithmetic in Rust
- ✅ Multiple approaches to safe math operations
This program contains a simple token system with:
initialize- Creates a token account with initial balancedeposit- Adds tokens (uses checked math)withdraw_vulnerable⚠️ - Withdraws with unchecked math (VULNERABLE)withdraw_secure✅ - Withdraws with checked math (SECURE)
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(())
}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!
Step-by-step exploit:
- Alice initializes a token account with 0 balance
- Alice calls
withdraw_vulnerable(1)- Current balance:
0 - Tries to subtract:
0 - 1 - Result:
18,446,744,073,709,551,615(u64::MAX)
- Current balance:
- Alice now has 18.4 quintillion tokens! 💰💰💰
// 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! 🚨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(())
}When using checked_sub():
- Attacker tries to withdraw 1 token with 0 balance
0.checked_sub(1)returnsNone(operation would underflow).ok_or(ErrorCode::InsufficientBalance)?convertsNoneto error- Transaction FAILS - no underflow, no infinite tokens! ✅
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 overflow3. 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 overflowUnderstanding the difference is crucial:
let x: u8 = 255;
let y = x + 1; // PANICS! "arithmetic operation overflow"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!
- 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
- 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)
| 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 | |
a.wrapping_add(b) |
Always wraps |
# Navigate to the program directory
cd 1-arithmetic-overflow
# Build the program
anchor buildThis 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!