Track 7 · The Breach Lab · lesson 12

Denial of service and gas griefing

15 min


Not every attack steals. Some just make sure nobody else can use the contract — and freezing a contract that holds funds can be as damaging as draining it.

The unbounded loop, again

You met this in track 4: a loop over an array anyone can grow eventually costs more gas than a block allows, and then it can never complete.

// Pays every investor in a loop. Anyone can join.
address[] public investors;

function payDividends() public {
  for (uint256 i = 0; i < investors.length; i++) {
      // one griefer adds thousands of addresses...
      payable(investors[i]).transfer(dividend);
  }
  // ...and now this loop exceeds the block gas limit. Forever.
}

The contract pushes to everyone in one transaction. An attacker inflates the list until the loop can't fit in a block, freezing dividends for everyone.

The unexpected-revert freeze

The second flavour is subtler. If a contract's progress depends on a payment to an address, and that address is a contract that reverts on receipt, the whole function can be jammed.

// An auction that refunds the previous top bidder.
function bid() public payable {
  require(msg.value > highestBid);
  // if the previous bidder is a contract that reverts here,
  // no new bid can ever succeed — the auction is frozen.
  payable(highestBidder).transfer(highestBid);
  highestBidder = msg.sender;
  highestBid = msg.value;
}

The auction refunds the old bidder inside bid(). A malicious bidder whose contract reverts on payment makes every future bid revert with it — they win by jamming.

Both fixes are the same idea, and it is the most useful defensive pattern in Solidity: favour pull over push. Don't have your contract send funds to many parties in one flow. Let each party come and take what they are owed.

It converts one shared point of failure into many independent ones — and an attacker can only jam their own.

Predict

Why does the pull pattern defeat both of these denial-of-service attacks?

Choose one answer

Check

An auction refunds the previous bidder inside bid(). How is it frozen?

Choose one answer

Worth remembering

  • Denial of service freezes a contract rather than stealing from it — and can be just as costly.
  • A loop over an attacker-growable array can be pushed past the block gas limit permanently.
  • A pushed payment to a contract that reverts on receipt can jam the function that sends it.
  • The fix for both is the pull pattern: let each party withdraw their own share.
  • Pull-over-push turns one shared failure point into many isolated ones.