BuildBot

Shipping on Chain

Security: the exploits that defined the industry

Lesson 1 of 5

What you'll learn

  • Reproduce a reentrancy attack against a naive withdraw and fix it with checks-effects-interactions
  • Recognise the three exploit classes behind most losses: reentrancy, access control, oracle manipulation
  • Describe what an audit is and what it is not

In June 2016 an attacker drained 3.6 million ETH from The DAO, the largest crowdfund in history at the time, using a bug that fits in five lines. The fallout split Ethereum into two chains. The bug is reentrancy, and understanding it is the entry ticket to writing contracts that hold money.

// Vulnerable: sends BEFORE updating the balance
function withdraw() external {
    uint256 amount = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: amount}("");   // hands control to the caller
    require(ok);
    balances[msg.sender] = 0;                             // too late
}

When the contract sends ETH to another contract, that contract's receive function runs in the middle of withdraw. If the attacker's receive calls withdraw again, the balance has not been zeroed yet, so it pays again. And again, until the vault is empty or gas runs out.

Checks, effects, interactions

The fix is an ordering discipline. Checks first (requires), effects second (write your own state), interactions last (calls to other contracts). Zero the balance before you send, and a re-entrant call finds nothing to take. Belt and braces: a reentrancy guard, a boolean lock set on entry and cleared on exit, which OpenZeppelin ships as nonReentrant.

function withdraw() external nonReentrant {
    uint256 amount = balances[msg.sender];   // check
    balances[msg.sender] = 0;                // effect
    (bool ok, ) = msg.sender.call{value: amount}("");   // interaction
    require(ok);
}

The other two classes

Access control. In 2017 the Parity multisig library had an initWallet function anyone could call; someone called it, became owner, and froze 513,000 ETH forever. Every state-changing function needs a deliberate answer to "who may call this", and the answer must be tested. Oracle manipulation. A lending protocol that prices collateral from a single pool's spot reserves can be tricked: borrow a huge amount, swap to move the price, borrow against the inflated collateral, swap back. Use time-weighted prices or a dedicated oracle network like Chainlink, never one pool's instant ratio.

What an audit actually is

An audit is a few experienced people reading your code for two to four weeks and reporting what they found. It raises the floor; it does not certify safety, and many exploited protocols were audited. Pair it with fuzzing, invariant tests, a bug bounty, and small limits at launch that grow with confidence.

Drain a vault, then patch it

Run it. The attacker deposits 1 ETH into a vault holding 10 and re-enters withdraw until it is empty. Then set FIXED = true and run again: the same attack takes back only the attacker's own deposit.

Loading editor…
Knowledge check

What makes reentrancy possible?

Next: the shape of a real dApp. Wallets in the browser, reads over RPC, writes through the wallet, and an indexer built from events.

Saved on this device. Sign in to sync your progress everywhere.