📋 What Is It?
Logic Errors (business-logic flaws) occur when a contract compiles cleanly, passes its happy-path tests, and appears to "work" - yet its implemented behaviour diverges from its intended behaviour. There is no reentrancy and no missing modifier; the code simply computes the wrong thing. The bug lives in the specification-to-implementation gap, so compilers, linters, and single-bug scanners all stay silent. On a public chain those wrong rules are permanent and tied directly to money: every accounting slip, mis-rounded division, and unhandled edge case is a standing invitation.
⚠️ Common Exploits
- First-Depositor Inflation: ERC-4626 donation makes a victim mint zero shares
- Rounding Leakage: Integer division truncates value to the user
- Reward Double-Count: Checkpoint never advances; same period paid twice
- Accounting Mismatch: Ledger credits requested, not received, amount
- Off-by-One: Wrong comparison operator bypasses a cap or window
- Order of Operations: Effects and interactions applied out of sequence
- Token Assumptions: Fee-on-transfer / rebasing tokens corrupt accounting
🔴 Attack Flow
↓
2. Find an unhandled edge case or wrong rounding
↓
3. Craft inputs (1 wei deposit + donation, repeat claim)
↓
4. Drive the contract into an unintended state
↓
5. IMPACT: Withdraw more than deposited - pool drained!
❌ Vulnerable Code
✅ Secure Code
✓ Prevention Checklist
- Write every core invariant down, then assert it
- Handle first-depositor/empty-pool (virtual shares, seed)
- Reject zero and degenerate inputs explicitly
- State rounding direction; always favour the protocol
- Use
Math.mulDivwith explicitFloor/Ceil - Credit balance-delta (received), not requested amount
- Advance reward checkpoints every accrual
- Support or reject fee-on-transfer / rebasing tokens
- Test exact-boundary inputs; review
<vs<= - Follow checks-effects-interactions ordering
- Reuse audited standards (ERC-4626, SafeERC20)
- Fuzz/invariant test; formally verify critical math
🔍 Detection & Tools
Analysis Tools:
Libraries & Verification:
How to Test:
- Foundry invariant: assert
totalAssets >= liabilities - Fuzz: deposit(x) then withdraw() never returns MORE than x
- Assert reward invariant: total paid <= total emitted
- Boundary tests for exactly-at-cap and one-past-cap inputs
- Formally verify share/mint and interest-accrual math
🌍 Real-World
- ERC-4626 vault inflation / donation class: A tokenised vault used empty-pool 1:1 math and read raw balances; an attacker deposited 1 wei, donated tokens directly to skew assets-per-share, and a later honest deposit rounded down to zero shares - absorbed by the attacker's single share.
- Accounting-error drains: A protocol's internal ledger drifted from real holdings (crediting before verifying a transfer, or ignoring a return value); attackers withdrew the difference until the pool was empty.
- Reward / emission miscalculation: A staking contract failed to advance its checkpoint or mis-scaled a fixed-point rate; the same accrual was claimed repeatedly, minting far beyond the emission schedule.
- Common thread: The code was reentrancy-safe and overflow-safe, but the math was wrong - a divergence tools cannot see and only specified invariants catch.
📌 Quick Tips
- DO NOT assume "compiles + tests pass" means correct
- DO NOT round in the user's favour
- DO NOT credit the requested amount blindly
- DO write and assert invariants first
- DO handle first-depositor and boundary cases
- DO reuse audited ERC-4626 / SafeERC20
- DO fuzz and formally verify critical math