Key Principle
Deployed contract code is immutable and runs in an adversarial public environment, so a single bug equals permanent, unrecoverable monetary loss. Every vulnerability below is a gap between what the developer assumes the EVM does and what it deterministically does. A contract "will execute exactly what is written, which is not always what the programmer intended." Defensive programming means coding to mechanism, not intent.
Why This Matters
All contracts are public, so any vulnerability is exploitable by anyone via a transaction, and "losses are almost always impossible to recover." Irreversibility + a public attack surface is the motivation for the entire antipattern catalog.
Good Examples
Each entry: mechanism → real exploit → fix.
1. Reentrancy (the DAO)
- Mechanism: The EVM can call arbitrary external contracts, handing control flow to an attacker mid-execution. When a contract sends ether before updating its own state, the recipient's fallback can re-invoke ("reenter") the withdrawal function while the stale balance check still passes — looping to drain the balance in one transaction. The bug is purely statement ordering. In
EtherStore.sol(Example 9-1) line 17 sends (msg.sender.call.value(_weiToWithdraw)()) before lines 18-19 decrement the balance. Attacker fallback re-callswithdrawFundswhileetherStore.balance > 1 ether. - Exploit: The DAO drained >$150M; fallout forced the contentious hard fork that split Ethereum and created Ethereum Classic (ETC).
- Fix (three, structural first): (1) Checks-Effects-Interactions — make external calls the last operation; decrement before sending so the reentrant call fails
require(balances >= _weiToWithdraw). (2)transfer2300-gas stipend blocks reentry[DATED 2018: avoid relying on the 2300-gas stipend; EIP-1884/later gas repricings made stipend-gated patterns fragile — use call{value:}() plus checks-effects-interactions and/or a reentrancy guard]. (3) Mutex — boolean set before the call,require(!mutex)at entry.
2. Arithmetic Over/Underflow (SafeMath)
- Mechanism: EVM integers are fixed-size and wrap silently (no revert). A
uint8holds [0,255]; storing 256 → 0; subtracting 1 from 0 → 255. Any unchecked arithmetic on user input is an exploit vector. TimeLock (Example 9-3): a publiclockTimelets an attacker callincreaseLockTime(2^256 - userLockTime)to overflow it to 0 andwithdraw()immediately. Token underflow (Example 9-4):require(balances[msg.sender] - _value >= 0)is always true for uint256 — it underflows to a huge positive, so a zero-balance user mints free tokens. - Exploit: BeautyChain (BEC) ERC20 (April 2018) — overflow minted over 57 undecillion tokens; PoWHC Ponzi lost 866 ether via underflow;
batchTransfer()overflow = CVE-2018-10299. - Fix: Route every op through SafeMath (
mul:assert(c/a == b);sub:assert(b <= a);add:assert(c >= a);divis exempt — can't over/underflow, EVM reverts on /0). Partial adoption leaves gaps; only routed ops are protected.[DATED 2018: superseded by Solidity ≥0.8.0 — over/underflow checks built in and revert by default; manual SafeMath unnecessary except inside explicit unchecked {} blocks or older pragmas.]
3. Unexpected / Forced Ether (selfdestruct, this.balance invariant)
- Mechanism: Ether can land in a contract without executing any code — not even the fallback, so "all incoming ether passed through my payable function" is false. Vector 1:
selfdestruct(target)removes bytecode and forces all stored ether onto any address with no defensive hook. Vector 2: pre-sent ether — contract addresses are deterministic (address = sha3(rlp.encode([account_address,transaction_nonce]))), so anyone can pre-fund a future address before its constructor runs. "The smoking gun for this vulnerability is the (incorrect) use of this.balance." - Exploit: EtherGame (Example 9-5) reads
this.balanceagainst milestones (3/5/10 ether). Force 0.1 ether via selfdestruct → balance never a multiple of 0.5 → milestones unreachable (DoS). Force ≥10 ether →claimReward()always reverts → rewards locked forever. - Fix: Track deposits in a
depositedWeivariable incremented only inside payable functions; never key logic onthis.balance. Use invariant checking with real invariants (totalSupplyof a fixed-issuance ERC20), not false ones (this.balance).[DATED 2018: selfdestruct largely neutered by EIP-6780 (2024 Dencun), restricting it to same-transaction-created contracts.]
4. DELEGATECALL Storage-Slot Collisions (Parity)
- Mechanism:
DELEGATECALLruns library code in the caller's context (msg.sender/msg.valueunchanged), against the caller's storage — but "state-preserving" means storage slots, not variable names. Solidity assigns state vars to slots sequentially in declaration order. If the library's slot model differs from the caller's, library writes hit whatever the caller put there — including the slot holding the library's own address, redirecting all future delegated execution to attacker code. - Exploit: FibonacciBalance/FibonacciLib (Examples 9-6/9-7): caller slot[0]=
fibonacciLibrary, slot[1]=calculatedFibNumber; librarysetStartthinks it writes its slot[0]startbut overwrites the caller's library address. Attacker callssetStart(int('<attack_addr>',16))to swap in a draining contract. Second Parity Multisig hack:WalletLibrarywas itself stateful; a user calledinitWalletdirectly, became owner, calledkill/suicide(_to), and every Parity multisig referencing it was bricked — all ether "permanently unrecoverable." Lib:0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4. - Fix: Declare reusable code with the
librarykeyword (notcontract) — removes persistent storage and forbidsselfdestruct. "whenever possible build stateless libraries."
5. Default Function Visibility → public (First Parity Multisig, ~$31M)
- Mechanism: Solidity functions with no specifier defaulted to
public. A forgotten specifier exposes privileged helpers (initializers, internal send helpers) externally. - Exploit: First Parity Multisig (~$31M):
WalletLibraryinitializersinitWallet/initMultiownedset the owners but specified no visibility, so anyone could re-run them on funded wallets, become owner, and drain. (Also HashForEther: implicitly-public_sendWinnings()bypasses the guard inwithdrawWinnings.) - Fix: "always specify the visibility of all functions in a contract, even if they are intentionally public." Modern
solcwarns on missing visibility.
6. Entropy Illusion (miner-manipulable block vars)
- Mechanism: Every transaction is a deterministic state transition, so there is no on-chain entropy. Block variables (next block hash/timestamp/number) are chosen by whoever mines that block, and are identical for every tx in a block (fire many max-bet txs to multiply one outcome).
- Exploit: A roulette paying "black" on even blockhash — a miner bets $1M, and if their solved block hashes odd, they withhold and re-mine until even, profitable whenever forgone block reward + fees < the payout. Arseny Reutov surveyed 3,649 live PRNG contracts, found 43 exploitable.
- Fix: Entropy must come from outside the chain — commit-reveal among peers, RANDAO, or a centralized randomness oracle.
[DATED 2018: on-chain pseudo-randomness now reads block.prevrandao (beacon-chain RANDAO mix, post-Merge 2022); still proposer-biasable and NOT safe for high-value randomness — external VRF oracles still required.]
7. External Contract Referencing (address-cast to malicious code)
- Mechanism: "In Solidity, any address can be cast to a contract, regardless of whether the code at the address represents the contract type being cast." A contract running a library reference set at deploy time runs whatever code lives at that address — not the audited source. Audit-of-source ≠ audit-of-runtime.
- Exploit:
EncryptionContract(Example 9-8) stores a non-publicencryptionLibraryset in the constructor; the deployer can point it atRot26Encryption(no-op → plaintext leaked), aPrintcontract, or a contract lacking the selector (its fallback runs arbitrary code). The honey-pot inversion:Private_Banklooks reentrancy-vulnerable but its constructor-suppliedTransferLogtraps the would-be exploiter (one reddit user lost 1 ether). - Fix: (1) Instantiate with
newat deploy time. (2) Hardcode addresses. (3) Make external addresses public so users can inspect them (a private address "can be a sign of someone behaving maliciously"). (4) If changeable, add time-lock and/or voting. Trade-off:new/hardcoding kill upgradeability.
8. Short Address Attack (ABI under-length padding)
- Mechanism: ABI encoding right-pads short parameters with zeros at the end. Submit an address one byte short and the dropped byte shifts the trailing
uint256left, multiplying it ×256 per missing byte. Targets the off-chain application (e.g. an exchange), not the contract. - Exploit:
transfer(address,uint)of 100 REP to0x…deadde(1 byte short): EVM appends trailing00, address reads0x…deadde00, value reads56bc75e2d6310000000= 25600 tokens (100×256). Exchange thinks 100 withdrawn; 25600 leave. - Fix: Validate input length in the off-chain app before sending (the real fix). Ordering value before address can blunt some forms. "the EVM will add zeros to the end of the encoded parameters to make up the expected length."
9. Unchecked CALL/send Return Values
- Mechanism:
sendand theCALLopcode return a Boolean and do not revert on failure; state after the call proceeds as if it succeeded. A failed payout that still flips a "paid" flag converts a delivery failure into theft. - Exploit:
Lotto.sendToWinner()—winner.send(winAmount)return ignored, thenpayedOut = trueflips even on failure; nowwithdrawLeftOver'srequire(payedOut)passes and anyone drains the winner's funds. EtherpotLotto.sol(Example 9-9) marksisCashed = trueregardless of send result; King of the Ether is the same bug. - Fix: Check the Boolean (
require(winner.send(...))), or usetransfer(reverts on failure), or the withdrawal pattern so one failed transfer corrupts only that user's state.
10. Race Conditions / Front-Running
- Mechanism: A public mempool + miner-controlled, fee-ordered inclusion. An attacker reads a pending profitable tx, copies its data, resubmits with higher
gasPrice, lands first. Miners are the worst adversary — "uniquely incentivized to run the attacks themselves (or can be bribed)." - Exploit:
FindThisHash.sol(Example 9-10) pays 1,000 ether for a SHA-3 preimage; the attacker copies the visiblesolve("Ethereum!")and outbids gasPrice — the honest user "will get nothing." ERC20approverace: Alice resets Bob's allowance 100→50; Bob front-runstransferFrom(100), then spends the new 50 = 150 total. Bancor's on-chain price curve is arbitraged the same way. - Fix: Cap
gasPrice(stops users, not miners); commit-reveal (hides contents, stops both classes; can't conceal value — ENS over-sends and refunds); submarine sends conceal value[DATED 2018: CREATE2 shipped in Constantinople (2019), submarine sends now implementable]. Forapprove: set allowance to 0 first, or useincreaseAllowance/decreaseAllowance.[DATED 2018: PoW miners → PoS proposers (Merge 2022); front-running now formalized as MEV with PBS; gasPrice ordering is now EIP-1559 priority-fee ordering — worse, not gone.]
11. tx.origin Authentication
- Mechanism:
tx.originis the address at the bottom of the call stack, not the immediate caller. Authenticating with it means any contract the victim calls inherits the victim's authority. - Exploit: Phishable.sol (Example 9-13): owner sends a tx to an attacker contract whose fallback calls
withdrawAll(attacker);require(tx.origin == owner)still passes (origin is the owner,msg.senderis the attacker) → funds drain. Works because public contract source is not visible by default. - Fix: Authorize on
msg.sender, nevertx.origin. Sole legitimate use:require(tx.origin == msg.sender)restricts to direct EOA calls.
12. Misnamed Constructor (Rubixi)
- Mechanism: Before Solidity 0.4.22 a constructor was a function whose name matched the contract name (exact string match). Rename the contract without renaming the function and the "constructor" silently becomes an ordinary callable public function running privileged init.
- Exploit:
OwnerWalletwith lowercaseownerWallet— "any user can call the ownerWallet function, set themselves as the owner, and then take all the ether." Rubixi: renamed fromDynamicPyramidwithout renaming its constructor, letting any user become "creator" and claim fees. - Fix: Use the
constructorkeyword.[DATED 2018: constructor keyword mandatory as of Solidity 0.5; named-function constructors no longer compile — this class cannot occur in modern code.]
13. Uninitialized Storage Pointers
- Mechanism: Complex local variables (structs, arrays) default their data location to
storage, not memory. An uninitialized local storage reference aliases slot[0] — clobbering whatever state variable is declared first, bypassing guards. - Exploit:
NameRegistrar(Example):unlocked→slot[0];NameRecord newRecord;(uninitialized) aliases slot[0], sonewRecord.name = _namewrites_namestraight intounlocked. Input0x…0001flips the permanent lock to true. Weaponized as honey pots (OpenAddressLottery, CryptoRoulette). - Fix: Always explicitly initialize local struct/array vars or mark them
memory; treat the compiler warning as an error (Mist 0.10 refused to compile).[DATED 2018: hard compile error since Solidity 0.5; this class no longer compiles.]
14. Precision / Truncation Errors
- Mechanism: "As of this writing (v0.4.24), Solidity does not support fixed-point and floating-point numbers." Integer division truncates toward zero; when divisor > dividend the result is 0, and a later multiply yields 0 — silently (no revert). Solidity "guarantees to perform operations in the order in which they are written," so divide-first destroys precision.
- Exploit:
FunWithNumbers:uint tokens = msg.value/weiPerEth*tokensPerEth→ 200 wei buys 0 tokens; selling 29 tokens → 2 ether (rounds down), breaking finer ERC20decimals. - Fix: (1) Invert/enlarge the ratio (store
weiPerToken, large). (2) Multiply before dividing (msg.value*tokensPerEth/weiPerEth). (3) High-precision uint256 intermediates, scale down only at the boundary (the rationale behind ERC20decimals). Ref: DS-Math ("wads"/"rays").
Bonus: Block Timestamp Manipulation
- Mechanism:
block.timestamp(now) is set by the sealing miner, bounded only by monotonicity and a near-future limit. Inside that window the miner picks the value. - Exploit: Roulette.sol (Example 9-11) wins if
now % 15 == 0— a miner-player seals a block with that timestamp. GovernMental Ponzi: a miner set a future timestamp to falsely qualify as last player. - Fix: For elapsed time use
block.number× avg block time ("1 week ≈ 60480 blocks" at 10s blocks; BAT ICO used this). For randomness use commit-reveal.[DATED 2018: post-Merge timestamps set by the slot proposer, tightly bounded to ~12s slots — window narrowed not eliminated.]
Counterpoints
- Several classes are dead in modern code: misnamed constructors (Solidity 0.5 keyword), uninitialized storage pointers (0.5 compile error), arithmetic over/underflow (0.8 built-in checks). The underlying lesson — never rely on conventions the compiler doesn't enforce — stands.
- The
transfer/send2300-gas stipend, once the recommended reentrancy defense, is now fragile after gas repricings; favorcall{value:}()+ checks-effects-interactions + reentrancy guard.
Key Quotes
"Smart contract code is unforgiving. Every bug can lead to monetary loss." — Antonopoulos & Wood, Chapter 9
"It is good practice for any code that performs external calls to unknown addresses to be the last operation… known as the checks-effects-interactions pattern." — Antonopoulos & Wood, Chapter 9
"The smoking gun for this vulnerability is the (incorrect) use of this.balance." — Antonopoulos & Wood, Chapter 9
"All transactions on the Ethereum blockchain are deterministic state transition operations... there is no source of entropy or randomness in Ethereum." — Antonopoulos & Wood, Chapter 9
Rules of Thumb
- External calls last; update state first (checks-effects-interactions).
- Never key logic on
this.balance— track deposits yourself. - Always specify function visibility explicitly.
- Always check
send/callreturn values; prefer the withdrawal pattern. - Authorize on
msg.sender, nevertx.origin. - No on-chain randomness — import entropy via commit-reveal/RANDAO/oracle/VRF.
- Build stateless libraries; never share a self-destructable stateful library.
- Multiply before dividing; work in smallest units internally.
- Validate input length off-chain before constructing transactions.
- Treat compiler warnings as errors.
Related References
- Secure Design Patterns - the defensive toolkit that prevents these classes
- Solidity Essentials — Writing Immutable, Self-Owning Contracts - language features (visibility, storage layout, SafeMath)
- EVM Internals — Stack Machine, State, Gas, and Bytecode - DELEGATECALL, storage slots, ABI encoding, gas
- Tokens (ERC-20, ERC-721, Fungibility, Standards) - ERC20/721 specific failure modes (approve race, stuck tokens)
- Rules of Thumb — Heuristics Across All Chapters - consolidated heuristics
Diagram
