defi

What Is a Re-Entrancy Attack? A Beginner's Guide

Learn what a re-entrancy attack is, how the DAO hack exploited it, and how to prevent this vulnerability with checks-effects-interactions and reentrancy guards.

What Is a Re-Entrancy Attack? A Beginner's Guide

A re-entrancy attack is a smart contract vulnerability that allows an attacker to repeatedly call a function before the contract updates its internal state, draining funds in the process. This exploit became famous after the 2016 DAO hack, which led to the loss of millions of dollars worth of Ether. Understanding how re-entrancy works is essential for any developer building decentralized applications (dApps) or for investors who want to assess the security of a protocol.

How Does a Re-Entrancy Attack Work?

A re-entrancy attack exploits the order of operations within a smart contract function. Typically, a vulnerable contract performs an external call (sending Ether to another address) before updating its own balance or state variables. This gives the attacker an opportunity to hijack the flow of execution.

The Basic Mechanism

Consider a simple smart contract that allows users to withdraw their deposited Ether. A naive implementation might look like this:

function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    msg.sender.call{value: amount}("");
    balances[msg.sender] -= amount;
}

The problem is that the external call (msg.sender.call{value: amount}("")) is made before the balance is deducted. If the recipient is another smart contract, its fallback function can call withdraw again before the original call finishes. Each recursive call satisfies the require check because the balance hasn’t been updated yet, allowing the attacker to withdraw more Ether than they deposited.

Analogy: The Bank Teller Trick

Imagine a bank where you can withdraw money by presenting your passbook. A careless teller hands you cash, then stamps your passbook to deduct the amount. If you quickly hand the cash back and ask for another withdrawal before the teller stamps, you can keep getting money. The re-entrancy attack works exactly like that trick—the contract "hands over" Ether before "stamping" the ledger.

The DAO Hack: A Famous Re-Entrancy Attack

The most notorious example of a re-entrancy attack is the 2016 DAO hack. The DAO (Decentralized Autonomous Organization) was a smart contract that pooled investor funds and allowed voting on proposals. Its withdrawal function suffered from the same flaw described above.

An attacker wrote a malicious contract that called the DAO’s split function, which sent Ether to the attacker’s contract. The attacker’s fallback function then called split again, recursively extracting Ether until the DAO was drained. The total loss was approximately 3.6 million Ether—a massive sum at the time. This event led to a hard fork of Ethereum, creating Ethereum (ETH) and Ethereum Classic (ETC).

⚠️ Warning: A common mistake beginners make is assuming that using transfer() instead of call.value() eliminates re-entrancy risk. While transfer() forwards only 2300 gas—enough to prevent complex re-entrancy—it can still be abused if the recipient contract uses a low-gas fallback, or if the Ethereum gas costs change in future upgrades. Always use proper guards instead of relying on gas limits.

Common Vulnerabilities That Enable Re-Entrancy Attacks

Several coding patterns make smart contracts susceptible to re-entrancy attacks. Recognizing them helps developers write safer code.

  1. State changes after external calls – The most common cause. Any function that sends Ether or calls an external contract should update its internal state before making the call.
  2. Using call.value() without checks – The low-level call function forwards all remaining gas by default, giving the recipient ample room to execute recursive calls.
  3. Unprotected fallback functions – Contracts that accept Ether but do not restrict what can happen in the fallback can be used as re-entrancy vectors.
  4. Cross-function re-entrancy – An attacker might call a different function within the same contract that shares state variables, exploiting the same out-of-date values.
Common VulnerabilityDescriptionTypical Impact
State after external callBalance deducted after sending EtherUnlimited draining of funds
call.value() with full gasRecipient gets all remaining gasEnables complex re-entrancy loops
Missing re-entrancy guardNo mutex lock on sensitive functionsRepeated entry to same function

How to Prevent Re-Entrancy Attacks

Developers have multiple tools to prevent re-entrancy attacks. The most widely recommended approaches are listed below.

Checks-Effects-Interactions Pattern

This pattern enforces a strict order: first check all conditions (e.g., require), then update the contract’s state (e.g., deduct balances), and finally perform interactions (e.g., send Ether). By updating state before external calls, any re-entrancy attempt will see an already-deducted balance and fail the require check.

function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;  // Effect first
    msg.sender.call{value: amount}(""); // Interaction later
}

Reentrancy Guard (Mutex)

A reentrancy guard is a boolean variable that prevents a function from being entered twice within the same execution context. OpenZeppelin’s ReentrancyGuard contract implements this pattern. The guard locks before the external call and unlocks after it completes, blocking recursive calls.

Using transfer() or send() with Caution

The transfer() function forwards only 2300 gas, which is insufficient for a re-entrancy loop. However, this is not a foolproof solution because gas costs can change in future hard forks, and some patterns (like withdraw with multiple recipients) may require more gas. Modern best practice favors the checks-effects-interactions pattern combined with a reentrancy guard.

Conclusion

A re-entrancy attack remains one of the most devastating vulnerabilities in smart contract development. By understanding the root cause—external calls made before state updates—developers can adopt proven defenses like the checks-effects-interactions pattern and reentrancy guards. Whether you are writing your first smart contract or auditing a complex DeFi protocol, keeping re-entrancy at the top of your risk checklist will save you and your users from catastrophic losses.