Smart Contract Security: Common Vulnerabilities

Smart Contracts, DeFi & Web3 Security
Course 2 · Chapter 7 · 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.

Pre-0.8 behavior — silently wraps, no error
uint8 balance = 0; balance -= 1; // wraps silently to 255 — no revert, no warning

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.

A Real, Rare Case: the Language Itself Changed Solidity 0.8.0 made overflow and underflow checks the automatic, built-in default for every arithmetic operation — the exact behavior SafeMath used to bolt on manually now happens without any library at all, and 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.

Jul 2017
A first, real Parity multisig wallet vulnerability lets an attacker drain a significant amount of ETH directly from several wallets.
Nov 2017
A separate, real incident: many Parity multisig wallets shared one common "library" contract holding their core logic (a design choice made to save on deployment gas costs, per Chapter 2). That library contract's own initialization function had been left callable by anyone. A user triggered it, accidentally becoming its "owner" — and then, apparently in an attempt to understand what had happened, triggered the library contract's own 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.
Why "Permanently" Isn't an Exaggeration This connects directly to Chapter 3's own immutability discussion: once the shared library contract was destroyed, there was no code left at that address for the dependent wallets to delegate to — not a bug that could be patched, not a function anyone could call to reverse it. The frozen funds are, to this day, still inaccessible. A missing access-control check on an ordinary web application might expose data or allow an unauthorized action that can later be detected and undone; the equivalent gap in an immutable, publicly-callable smart contract can produce consequences with no possible undo at all.

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.

1
A vulnerable lending protocol reads its "price" for some asset directly from a single AMM pool's current ratio, rather than a more robust, manipulation-resistant price source.
2
An attacker takes out a large flash loan and dumps a huge amount of one token into that same AMM pool, temporarily distorting its own price ratio, exactly like Chapter 4's own worked swap example.
3
The vulnerable protocol, reading that now-distorted price, lets the attacker borrow far more than they should genuinely be able to against their real collateral, or liquidate a position that shouldn't actually qualify.
4
The attacker reverses their original AMM trade, repays the flash loan, and keeps the profit — all within the exact same atomic transaction.

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.

Exercise 1
Using this chapter's own real Parity library-freeze incident and Chapter 3's own immutability material, explain specifically why deploying a single shared library contract that many wallets depend on creates a fundamentally different kind of risk than each wallet having its own fully independent copy of the same code.
Exercise 2
A contract uses 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.
Exercise 3
Using this chapter's own oracle manipulation flow, explain precisely which real property from Chapter 4 makes step 4 (repaying the flash loan and keeping the profit) reliably possible for the attacker, rather than a risky gamble that might leave them owing an unpaid loan.

Quick Reference

  • Cross-function reentrancy — a subtler reentrancy variant re-entering a different, shared-state function; ReentrancyGuard defends 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.origin vs. msg.sendertx.origin resolves through intermediate contracts, enabling a real phishing pattern; msg.sender doesn'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.