Back

SC03: Logic Errors - Overview

What Are Logic Errors?

Logic Errors (also called business-logic flaws) occur when a smart contract compiles cleanly, passes its happy-path tests, and appears to “work”—yet its implemented behaviour diverges from its intended behaviour. There is no memory-safety bug, no reentrancy, no missing access-control modifier; the code simply computes the wrong thing. An attacker who understands the intended invariant better than the developer did can drive the contract into a state the designer never meant to allow, and extract value on the way.

Because the bug lives in the specification-to-implementation gap rather than in a language feature, a compiler, a linter, and even a reentrancy scanner will all stay silent. The contract is internally consistent—it just enforces the wrong rules. On a public blockchain those wrong rules are permanent, adversarially probed, and directly tied to money: every accounting slip, every mis-rounded division, every unhandled edge case is a standing invitation.

Core Concept

Intended behaviour (the specification):
  Deposit         -> user's recorded balance increases by exactly the amount sent
  Shares minted   -> proportional to assets deposited vs. total assets held
  Reward          -> accrues linearly with stake and elapsed time, no double-count
  Withdraw        -> user can never remove more value than they are entitled to
  Rounding        -> always resolved in the protocol's favour, never the user's
  Invariant       -> sum(balances) == totalSupply, always, after every operation

Logic error (the implementation diverges):
  Deposit         -> balance updated before/without validating the transfer amount
  Shares minted   -> first depositor sets an exchange rate an attacker can inflate
  Reward          -> accrual re-runs on each call, paying the same period twice
  Withdraw        -> uses a stale or attacker-influenced price to over-pay
  Rounding        -> integer division truncates value TO the user (leakage)
  Invariant       -> broken silently; totalSupply and real holdings drift apart

Why It's Critical for Smart Contracts

Smart contracts concentrate several conditions that make logic errors uniquely dangerous:

Why Does This Matter?

Business Impact

Technical Impact

Technical Context

Common Logic-Error Scenarios in Smart Contracts

1. Incorrect Accounting / Balance Updates

// The recorded balance and the value actually held diverge:
function deposit(uint256 amount) external {
    balances[msg.sender] += amount;      // credited BEFORE the transfer is verified
    token.transferFrom(msg.sender, address(this), amount); // return value ignored
}
// If transferFrom silently fails or moves fewer tokens (fee-on-transfer),
// the ledger now claims more than the contract holds.

Risk: The internal ledger overstates real holdings; the last users to withdraw find the pool empty.

2. Rounding & Precision Errors (Integer Division Truncation)

// Solidity has no floats: division truncates toward zero.
uint256 shares = (amount * totalShares) / totalAssets;   // rounds DOWN
// Minting rounds down (good for the protocol), but a WITHDRAW that also
// rounds down the assets owed while the SAME rounding is applied to shares
// burned can hand the user a fraction of value for free, repeatedly.

Risk: Rounding in the user's favour leaks value on every call; at scale it is a slow drain, and with a crafted amount it can be amplified.

3. First-Depositor / Empty-Pool Edge Case (Inflation Attack Class)

// Vault share math when totalSupply == 0:
shares = (totalSupply == 0)
    ? amount                                   // 1:1 for the first depositor
    : (amount * totalSupply) / totalAssets;

// Attacker: deposit 1 wei -> mint 1 share, then DONATE a large amount
// directly to the vault (transfer, not deposit). Now 1 share is "worth"
// the whole balance. A later depositor's shares round DOWN to zero.

Risk: The classic ERC-4626 inflation / donation attack: a victim deposits real assets and receives zero (or too few) shares, which the attacker then redeems.

4. Flawed Reward / Interest / Fee Calculation

// Reward accrual that forgets to update the checkpoint:
function claim() external {
    uint256 owed = stake[msg.sender] * rate * (block.timestamp - start);
    reward.mint(msg.sender, owed);
    // BUG: 'start' (or a per-user lastClaim) is never advanced,
    // so the same elapsed period pays out again on the next call.
}

Risk: The same period is paid repeatedly, or a mis-scaled rate mints far beyond the intended emission, devaluing every holder's stake.

5. Wrong Order of Operations / State Transition

// Effects applied out of order relative to checks:
function withdraw(uint256 amount) external {
    (bool ok, ) = msg.sender.call{value: amount}("");   // interaction first
    require(ok);
    balances[msg.sender] -= amount;                     // effect last
}
// Even ignoring reentrancy, mixing up the intended sequence
// (validate -> update -> interact) corrupts the state machine.

Risk: The contract enters states the author assumed unreachable; invariants that “obviously” held are violated.

6. Off-by-One and Wrong Comparison Operators

require(totalMinted + qty <= MAX_SUPPLY);   // correct: allows up to the cap
require(totalMinted + qty < MAX_SUPPLY);    // off-by-one: cap can never be reached
if (block.timestamp > deadline) revert();   // should this be >= ? a whole second differs

Risk: Callers exceed a cap, mint one too many, or act one unit outside an intended boundary.

7. Unchecked Assumptions About Tokens

// Assuming amount-in == amount-received:
uint256 before = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
// Fee-on-transfer token delivers (amount - fee); rebasing token changes
// balances out from under you. Crediting 'amount' over-states the deposit.

Risk: Fee-on-transfer and rebasing tokens break the “what I asked for is what I got” assumption, corrupting accounting.

Where Logic Errors Hide

AreaTypical Logic ErrorConsequence
Accounting / ledgerBalance updated without verifying the real transferLedger overstates holdings; pool drained
Share / mint mathFirst-depositor rate set, then inflated by donationVictim mints zero shares (ERC-4626 inflation)
Rewards / interestCheckpoint not advanced; period double-paidEmissions drain, token devalued
Rounding / precisionDivision truncates in the user's favourContinuous value leakage
State transitionsEffects and checks applied out of orderUnreachable states reached; invariants broken
BoundariesOff-by-one, wrong comparison operatorCaps bypassed, windows escaped
Token assumptionsamount-in assumed equal to amount-receivedFee-on-transfer / rebasing corrupt accounting

Real-World Impact

The incidents below are described as classes of failure that have recurred across DeFi. The point is the shape of the bug, not the box score of any single event.

Case Study 1: Accounting-Error Drains

Logic Error:

Impact:

Root Cause: The implemented accounting diverges from the intended invariant internal_ledger == real_holdings, and no test or on-chain check ever asserts that invariant.

Case Study 2: ERC-4626 Vault Inflation / Donation Attack Class

Logic Error:

Impact:

Root Cause: An unhandled empty-pool edge case combined with rounding that favours the existing share holder. Modern designs mitigate with virtual shares/assets (a decimals offset), a minimum initial deposit, or seeding the vault at deployment.

Case Study 3: Reward / Emission Miscalculation Exploits

Logic Error:

Impact:

Root Cause: The reward formula and its bookkeeping do not match the intended “each unit of stake earns for each unit of time exactly once” invariant, and property tests never asserted “total paid ≤ total emitted.”

Prevalence and Statistics

Logic Errors sit near the top of the OWASP Smart Contract Top 10 (2025) and are, by most post-mortem tallies, among the largest sources of realised loss in DeFi—precisely because they evade the tooling that catches memory and access-control bugs.

Rather than cite a single dollar figure (which shifts with every incident), the defensible picture is:

Note: precise loss totals differ between trackers and change with every new incident. Treat any single figure as illustrative; the durable takeaway is that logic errors are common, expensive, and invisible to tools that only check for known vulnerability shapes.

Common Misunderstandings

Myth 1: “It compiles and the tests pass, so the logic is correct”

Reality: Compilation proves the code is well-formed, and happy-path tests prove it works for the inputs you imagined. Logic errors live in the inputs you didn't imagine—zero amounts, empty pools, first depositor, adversarial ordering. Correctness is a property you must state and test, not a by-product of compiling.

Myth 2: “Rounding is a rounding error—it's negligible”

Reality: On integer-only arithmetic, rounding direction is a security decision. A fraction of value leaked per transaction, multiplied by unlimited attacker-controlled transactions, is a drain. Always round in the protocol's favour and prove it.

Myth 3: “An audit will catch our business-logic bugs”

Reality: Auditors are far more likely to find logic errors than tools are, but they cannot verify an invariant you never wrote down. Undocumented intended behaviour is unauditable behaviour. Specify first, then audit against the specification—and prefer multiple independent audits.

Myth 4: “A reentrancy guard / SafeMath means we're safe”

Reality: Those defend against specific, known bug classes. A logic error can be perfectly reentrancy-safe and overflow-safe while still computing the wrong number. The math being safe is not the math being right.

Myth 5: “Any ERC-20 will behave like a standard ERC-20”

Reality: Fee-on-transfer and rebasing tokens violate “amount sent equals amount received” and “my balance only changes when I move it.” Code that assumes the standard behaviour mis-accounts the moment such a token is used.

Myth 6: “We can just patch it if something's wrong”

Reality: Deployed contracts are immutable unless you built (and secured) an upgrade path in advance. By the time a logic error is observed on-chain, it is usually being actively exploited. Prevention, not patching, is the operative control.

How Logic Errors Differ from Related Issues

AspectLogic Errors (SC03)Access Control (SC01)Reentrancy (SC05)
Root causeImplementation diverges from intended logicMissing/incorrect authorisationState changed after an external call
Where it livesThe math and state machine itselfPermission checks / modifiersCall ordering vs. state updates
Typical fixSpecify, test invariants, round correctlyEnforce roles / ownershipChecks-effects-interactions, guards
DetectionProperty/fuzz testing, formal verification, auditAccess-control reviewReentrancy analysis, guards

Key Takeaways

  1. Logic errors are the gap between intended and implemented behaviour—the code is valid but computes the wrong thing.
  2. Tools stay silent—compilers, linters, and single-bug scanners do not know your business rules; you must specify and test them.
  3. Rounding direction is a security decision—always resolve integer truncation in the protocol's favour.
  4. Edge cases are where value leaks—first depositor, empty pool, zero amount, and off-by-one boundaries must be handled explicitly.
  5. Immutability raises the stakes—there is rarely a second chance, so invariants must be proven before deployment.

How to Identify if You're Vulnerable

Ask these questions about your contract:

If you answered “no” or “not sure” to several of these, your contract likely harbours an exploitable logic error today.

Next Steps