Skip to main content
Sigvex

Red Team / Blue Team EVM · Part 3: The Bugs That Move Money

The attacker's shortlist of value-extraction bugs — reentrancy, unchecked calls, precision loss, and oracle manipulation — with enough mechanism to spot them in your own code.

Red Team / Blue Team EVM · Part 3: The Bugs That Move Money

EVM Red Team / Blue Team — a six-part path from reading a stranger’s deployed bytecode to shipping a contract that survives contact with an attacker.

  1. Read a Deployed Contract Like an Attacker
  2. Mapping the Attack Surface
  3. Part 3: The Bugs That Move Money
  4. Owning the Contract: Access Control and Upgrades
  5. Blue Team: From Bytecode to Triaged Findings
  6. Blue Team: Hardening and Holding the Line

Part 2 left you with a map: entry points, the functions that touch balances, the external calls, the trust boundaries. Now we spend that map. Most catastrophic EVM losses trace back to a short list of bugs that move value directly out of a contract. An attacker doesn’t hunt for novelty — they run down the shortlist, because it keeps paying. This post is that shortlist, with enough mechanism that you recognize each pattern when it’s your code under review.

Think of it as a tour, not a manual. Each class has a dedicated deep-dive linked inline; the goal here is pattern recognition.

Reentrancy: the call that comes back

The single most durable EVM bug is a contract that talks to the outside world before it finishes updating itself. When you send ETH with call, the recipient runs code. If that recipient calls back into you before your state reflects the first interaction, you process the second call against stale state.

The classic shape is a withdraw function that pays out and then bookkeeps:

function withdraw() external {
    uint256 amount = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: amount}("");   // hands control to the caller
    require(ok, "transfer failed");
    balances[msg.sender] = 0;                            // too late — state cleared after the call
}

The attacker’s receive() calls withdraw() again. On the reentry, balances[msg.sender] still reads the full amount, so the contract pays a second time, a third, until it’s dry. The fix is old and boring: update state before the external call, or wrap the function in a reentrancy guard. Checks-Effects-Interactions is the name for doing it in that order.

The classic case is only the first variant. Cross-function reentrancy reenters through a different function that shares the same unguarded state — a per-function guard misses it. Read-only reentrancy is nastier: your contract stays consistent, but another protocol reads your view function mid-transaction, while your state is briefly wrong, and makes a bad decision on that reading. A guard that only protects state-changing functions won’t catch it. The full breakdown of all three is worth reading before you trust any single mitigation.

The signal an analyzer keys on is ordering: a storage write to a balance or accounting slot that happens after a CALL to an untrusted address in the same execution path. That “write-after-call” shape is visible in the bytecode without knowing what the function is named or what the source intended.

Unchecked and mishandled external calls

Every external call is a place where control and trust leave your contract. Two habits turn that into a loss.

The first is ignoring what the call reports. A low-level call returns a success boolean; if you drop it, a failed transfer looks identical to a successful one and your accounting drifts out of sync with reality. Some token contracts return false instead of reverting on a failed transfer — code that assumes reverts silently continues.

token.transfer(to, amount);           // return value dropped — did it actually move?
(bool ok, ) = target.call(data);      // ok never checked — failure looks like success

The second is the mindset behind it: assume the callee is hostile. When you call an address the caller supplied, you’re running code you didn’t write, chosen by someone who may profit from your failure. It can reenter you, revert to grief you, return crafted data, or consume all the gas you forwarded. Treat every return value as untrusted input, check it, and decide deliberately how much gas and control you’re willing to hand over.

The bytecode-visible signal is a CALL-family opcode whose returned success flag is never consumed by a following conditional — the value is produced and discarded rather than branched on.

Arithmetic and precision

Solidity 0.8 made arithmetic revert on overflow and underflow, which retired a whole generation of bugs where a subtraction wrapped around to a giant number and minted value from nothing. Pre-0.8 code — and any unchecked { } block — still carries that risk, so it’s worth knowing the shape. But the arithmetic bug that bites in modern code is subtler: precision loss.

The EVM has no floating point. Integer division truncates. Order the operations wrong and you throw away value:

uint256 reward = (amount / totalStaked) * rewardRate;   // divide first — truncates to zero for small amounts
uint256 reward = (amount * rewardRate) / totalStaked;    // multiply first — keeps precision

Divide-before-multiply is the canonical form, and it’s everywhere value gets split proportionally. The highest-stakes version lives in vaults that mint shares for deposits. If the share-price rounding favors the wrong party, an attacker can deposit, donate assets directly to the vault to inflate the price per share, and make a later depositor’s rounding-down mint them fewer shares than they paid for — an “inflation” or “donation” attack. Rounding direction is a security decision: round in favor of the protocol, never the user pulling value out.

The integer arithmetic deep-dive covers both the wraparound era and the precision traps that outlived it. An analyzer’s tell here is structural: a division opcode feeding a multiplication in the same expression, and unchecked regions where the reverting guardrails are switched off.

Price manipulation and flash loans

The last item on the shortlist is where the biggest recent numbers come from. A contract that needs to know a price — to value collateral, to settle a trade, to decide a liquidation — has to get that number from somewhere. If it reads the spot price from a single on-chain source, that number is only as trustworthy as the source is hard to move.

A single-DEX pool is not hard to move. Its price is just the ratio of two reserves, and anyone with enough capital can shove that ratio in either direction within one transaction. Which is where flash loans come in: they lend an attacker enormous capital with no collateral, on the sole condition that it’s repaid before the transaction ends. That removes the one barrier — needing money — that used to make price manipulation impractical. Borrow millions, skew the pool, trigger your victim’s mispriced logic, repay, keep the difference.

The mechanism deserves its own read; the flash loan anatomy walkthrough traces a full attack step by step, and oracle manipulation detection covers how single-source reads get flagged. The defense is well understood: use a manipulation-resistant price — a time-weighted average, or an aggregated oracle — rather than an instantaneous reserve ratio, and never trust a number that a single transaction can set. These patterns, along with the vault rounding above, are surveyed together in the DeFi-specific vulnerabilities overview.

The conceptual signal for an analyzer is a price or valuation derived from a live reserve read that then drives a value-moving decision — a swap, a mint, a liquidation — inside the same call, with no averaging or staleness check in between.

What ties the shortlist together

Reentrancy, unchecked calls, precision loss, and price manipulation don’t look alike in source, but they share a spine: each one lets an attacker extract more value than they put in, and each leaves a structural fingerprint in the bytecode — an ordering, a discarded flag, an opcode sequence, a data-flow path — that survives even when names and comments are gone. That’s exactly the property Part 5 exploits when it shows how to find these classes automatically instead of by eye.

First, though, there’s a category of loss that doesn’t require any of these bugs — where the attacker simply becomes the owner. Part 4 turns to access control and upgrades: who is allowed to move the money, and what happens when that answer is wrong.