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:

  1. What is a Reentrancy Attack?

  2. How Does It Work? (With Code Examples)

  3. Types of Reentrancy Attacks

  4. Real-World Exploits

  5. How to Prevent Reentrancy

  6. 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

solidity
Copy
// 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

solidity
Copy
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:

  1. Attacker deposits 1 ETH into VulnerableBank.

  2. Attacker calls withdraw(), triggering an ETH transfer.

  3. Before balances[msg.sender] = 0 executes, the attacker’s receive() function calls withdraw() again.

  4. 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 same balances mapping.

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.

solidity
Copy
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.

solidity
Copy
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() or send() (they have a 2300 gas limit, preventing reentrancy).

solidity
Copy
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

Popular posts from this blog

🔐 Cryptography in Solana: Powering the Fast Lane of Web3

Battle of the Decentralized Clouds: IPFS vs Arweave vs Filecoin Explained

Decentralization vs. Regulation: Where Do We Draw the Line?