Track 7 · The Breach Lab · lesson 6

Calls that fail silently

15 min


Solidity's low-level .call does not revert when the call it makes fails. It returns false — and if nobody looks at that false, execution carries on as though everything worked.

This is different from a normal function call, which propagates a revert upward. .call, .send, .delegatecall and .staticcall all hand you a boolean and step aside. Ignoring it is one of the most common real bugs in Solidity, and static analysers flag it on sight.

The bug

A rewards contract that pays out, marks you as paid, and never checks whether the payment landed:

function claim() public {
  uint256 owed = rewards[msg.sender];
  rewards[msg.sender] = 0;

  // If this fails, ok is false — and we ignore it.
  // The reward is now zeroed, but the ETH never left.
  msg.sender.call{value: owed}("");
}

The return value is discarded. A failed payout silently zeroes the reward, and the user is now owed nothing for money they never received.

Why "silent" is the dangerous word

A crash is loud. You see it, you fix it. A silent failure leaves the contract in a state that looks correct — the reward shows as paid — while the money sits undelivered. Nobody notices until someone reconciles the books, by which point the accounting is wrong in a dozen places.

Predict

Why does `require(ok)` after the call actually protect the user here?

Choose one answer
delegatecall is the same trap with higher stakesOptional

delegatecall runs another contract's code in your contract's storage context. Ignore its return value and you can silently corrupt your own state with someone else's logic.

It is the mechanism behind upgradeable proxies — and behind the 2017 Parity freeze, where a delegatecall to a library that had been self-destructed locked $280 million permanently. Upgradeability gets its own lesson later in this track.

Check

What does the low-level `.call` do when the call it makes reverts?

Choose one answer

Worth remembering

  • Low-level `.call`/`.send`/`.delegatecall` return false on failure instead of reverting.
  • Ignoring that boolean lets a failed operation pass silently while state says it succeeded.
  • `require(ok)` converts the silent failure into a revert that rolls back earlier state changes.
  • This pairs with checks-effects-interactions: state changed first means the revert cleans up correctly.
  • `delegatecall` is the highest-stakes version — it runs foreign code in your storage.