Find the Reentrancy Bug: Checks-Effects-Interactions in Solidity

Cryptocurrency & Blockchain Security Level 4/4 ~7 min August 16, 2026

The challenge

This Solidity vault sends ether to the caller before it updates the caller's balance. A malicious contract can call back in during that gap and drain the vault. Read the contract and type the name of the function with that flaw.

What you'll learn

  • Recognise an external call placed before a state update as a reentrancy sink
  • Apply the checks-effects-interactions pattern to audit a function
  • Explain how a malicious fallback re-enters a contract to drain funds
  • Tell a safe state-only function apart from one that makes an external call
  • Name the correct fixes: reorder effects before interactions or use a guard

Skills tested

Smart contract auditingSolidity source reviewReentrancy analysis

Prerequisites

  • Basic Solidity syntax (mappings, msg.sender, call)
  • How ether transfers can invoke a fallback function
  • What contract state (balances) is

How it works

Reentrancy is the classic smart-contract vulnerability behind some of the largest DeFi losses. It happens when a contract makes an external call before it finishes updating its own state. The external call can hand control to an attacker contract, which calls back into the original function before the state was settled - so the contract acts on stale data.

In Vault.sol, withdraw() reads bal = balances[msg.sender], then transfers the ether with msg.sender.call{value: bal}(""), and only afterwards sets balances[msg.sender] = 0. The low-level call triggers the recipient's fallback function. A malicious contract's fallback simply calls withdraw() again - and because its balance has not been zeroed yet, the require(bal > 0) check still passes and it is paid again. Repeating this loop drains the entire vault.

The principle being violated is checks-effects-interactions: do the checks (require), then apply effects (update balances), and only then perform interactions (external calls). deposit() follows this trivially - it updates state and makes no external call, so it is safe and serves as the distractor. The vulnerable function is withdraw, and the audit skill is spotting that the state update comes after the call.

Common mistakes

  • Naming deposit(). It only updates state and makes no external call, so it cannot be re-entered.
  • Blaming msg.sender.call generically. The call is fine; the bug is its position before the balance update. Name the function, not just the call.
  • Assuming Solidity 0.8 overflow checks help. Safe math does not prevent reentrancy - the order of operations does.
  • Thinking require(bal > 0) is the guard. It re-passes on every re-entry because the balance is still non-zero during the call.

How to defend against it

Always finish updating state before making an external call. Reorder withdraw() so the balance is zeroed first, and add a guard for defence in depth.

  • Follow checks-effects-interactions: set balances[msg.sender] = 0 before the call.
  • Use a reentrancy guard (for example OpenZeppelin's nonReentrant modifier) on functions that move funds.
  • Prefer pull-payment patterns over pushing ether inside complex logic.
  • Audit every external call/transfer for state that is still mutable when control leaves the contract.

Full solution

Pro and Max members unlock the complete step-by-step walkthrough.

Go Pro

Community stats

111 completions
79% success rate
M2F14M3 First blood

Related Daily Hacks

24,000+ Hackers 100+ Labs & Courses Free
Start Hacking Free