Smart Contract Security: Common Vulnerabilities
Chapter 3 introduced Checks-Effects-Interactions as a defense against one specific, real vulnerability — reentrancy. This chapter surveys the broader, real landscape: several more genuine vulnerability classes, each with a real, documented incident behind it, and each teaching a real, specific lesson about how smart contract security differs from ordinary software security.
Reentrancy, Revisited: Beyond a Single Function
Chapter 3 covered the classic case — a function re-entering itself before its own
state update completes. A subtler real variant is cross-function reentrancy:
the malicious callback doesn't call the same function again, it calls a different
function that happens to read or write the same shared state variable, before that state has
been updated. Checks-Effects-Interactions still helps, but as real defense-in-depth, OpenZeppelin
(Chapter 3) provides a dedicated ReentrancyGuard modifier — a real, simple
lock flag that blocks any re-entry into a protected function, from anywhere, for the
duration of the outer call, regardless of which specific function the attacker tries to re-enter
through.
Integer Overflow and Underflow: A Real, Language-Level Lesson
Solidity's uint types have a fixed bit width, and therefore a fixed maximum value.
In Solidity versions before 0.8.0, arithmetic that pushed a value past that
maximum (overflow) or below zero (underflow) didn't raise an error at all — it silently
wrapped around, exactly like an odometer rolling over. Subtracting 1 from a
uint already at 0 didn't fail; it silently became the type's own maximum possible
value instead.
This was a real, exploitable class of bug in production contracts — a balance check like
require(balance >= amount) could be defeated entirely if an underflowed balance
wrapped around to an enormous number, satisfying the check with a balance that should have been
impossible. The real, standard pre-0.8 defense was a library called SafeMath,
which wrapped every arithmetic operation in an explicit check that reverted on overflow/underflow
instead of wrapping.
balance -= 1 on a
zero-valued uint8 today reverts automatically. This is a genuinely notable, real
example of a vulnerability class serious enough that an entire language changed its own default
behavior specifically to close it, rather than leaving it as a convention developers had to
remember to apply themselves.
Access Control: A Real, Permanent Consequence
A real, well-documented incident shows exactly why an access control bug in a smart contract can be categorically worse than the equivalent bug in ordinary software.
selfdestruct. Because every dependent wallet's own logic pointed at that
one now-empty address, this single action instantly and permanently froze every wallet that
depended on it — widely reported at the time as well over 500,000 ETH, worth well over
$100 million, locked forever.
tx.origin vs. msg.sender: A Real, Solidity-Specific Trap
Solidity provides two different ways to identify who's calling a function: msg.sender
(Chapter 1) — the address that made the immediate call — and
tx.origin, which always resolves to the original, human-controlled address that
kicked off the entire transaction, even if it passed through several intermediate contract calls
first.
Using tx.origin for an authorization check is a real, documented mistake: a
malicious contract can trick a legitimate, authorized user into calling it (perhaps
disguised as something harmless), and then have that malicious contract call the real, sensitive
function on the user's behalf. Since tx.origin still resolves to the real user's own
address no matter how many contracts the call passed through, a check like
require(tx.origin == owner) passes — even though the actual, immediate caller
(msg.sender) is the attacker's own malicious contract, not the owner acting directly.
Using msg.sender instead closes this off entirely, since it would correctly show the
malicious contract as the immediate caller.
Oracle Manipulation: Combining Two Chapters Into One Real Attack
Chapter 4 covered how an AMM's own price is a direct, mechanical function of its pool's current token ratio — and Chapter 4 also covered flash loans, which let anyone temporarily command an enormous, uncollateralized sum within one atomic transaction. Put together, they describe a real, documented category of DeFi exploit.
This is precisely why Chapter 4 flagged flash loans as a real, documented component of several serious exploits: the vulnerability isn't really the flash loan itself, it's a protocol trusting a price source that can be temporarily moved within a single transaction.
Front-Running and MEV
A submitted transaction doesn't confirm instantly — it sits briefly in a public, visible holding area called the mempool before being included in a block. Because anyone can see a pending, profitable transaction before it confirms, a real, documented practice called front-running has emerged: a bot (or, in principle, the very miner or validator deciding block order) can spot a lucrative pending transaction and insert their own transaction ahead of it, paying a higher fee to guarantee it's processed first. This general category of profit extractable purely from a validator or bot's own ability to choose transaction ordering is often called MEV (Maximal Extractable Value) — a real, ongoing, actively studied concern across the whole Ethereum ecosystem, not a rare edge case.
Hands-On Exercises
Three exercises applying this chapter's real vulnerability classes before Chapter 8 covers several more real, documented exploits in full case-study depth.
require(tx.origin == owner) to protect a sensitive function.
Describe, step by step, how a malicious contract could exploit this even though the real
owner never intentionally called the sensitive function directly.
Quick Reference
- Cross-function reentrancy — a subtler reentrancy variant re-entering a different, shared-state function;
ReentrancyGuarddefends broadly. - Integer overflow/underflow — silently wrapped before Solidity 0.8.0; SafeMath was the real pre-0.8 workaround, now built into the language by default.
- Access control (the Parity freeze, Nov 2017) — a callable, uninitialized shared library let a user accidentally trigger
selfdestruct, permanently freezing well over 500,000 ETH with no possible fix. tx.originvs.msg.sender—tx.originresolves through intermediate contracts, enabling a real phishing pattern;msg.senderdoesn't.- Oracle manipulation — a flash loan temporarily distorts an AMM price a vulnerable protocol trusts directly, exploited and repaid within one atomic transaction.
- Front-running / MEV — visible pending transactions in the public mempool can be reordered ahead of for profit.