Skip to content

Classic Reentrancy

id: LS09C
title: Classic Reentrancy
baseSeverity: C
category: reentrancy
language: solidity
blockchain: [ethereum]
impact: Full or partial contract balance drain
status: draft
complexity: medium
attack_vector: external
mitigation_difficulty: easy
versions: [">=0.4.0", "<=0.8.25"]
cwe: CWE-841
swc: SWC-107

๐Ÿ“ Description

  • Classic reentrancy occurs when a smart contract sends Ether to an external contract using a low-level call (e.g., call, send, or transfer) before updating its internal state.
  • If the recipient is a malicious contract, it can recursively call back into the vulnerable function, repeating the withdrawal process before the state is updated, thereby draining funds or bypassing logic constraints.
  • This vulnerability was the root cause of the infamous DAO hack in 2016, resulting in over $60M in stolen Ether and the eventual Ethereum hard fork.

๐Ÿšจ Vulnerable Code

pragma solidity ^0.8.0;

contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        // โŒ Send Ether before updating state
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0; // State update happens too late
    }
}

๐Ÿงช Exploit Scenario

  1. Attacker deposits 1 ETH into VulnerableVault.
  2. Attacker calls withdraw(). During the call, their fallback function is triggered.
  3. In the fallback, attacker recursively calls withdraw() again.
  4. Since balances[msg.sender] has not yet been set to 0, they withdraw again.
  5. This repeats until the contract is drained of ETH.

Assumptions:

  • The recipient is a contract capable of reentry.
  • The vulnerable contract uses call, send, or transfer before updating state.

โœ… Fixed Code

pragma solidity ^0.8.0;

contract SafeVault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw");

        balances[msg.sender] = 0; // โœ… Update state before sending
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
}

๐Ÿงญ Contextual Severity

- context: "Default"
  severity: C
  reasoning: "Allows attacker to drain contract completely under realistic assumptions."
- context: "ReentrancyGuard in place"
  severity: L
  reasoning: "Attack vector mitigated if guarded appropriately."
- context: "Only non-value-returning external calls"
  severity: M
  reasoning: "Risk reduced if external calls donโ€™t affect user balances or privileges."

๐Ÿ›ก๏ธ Prevention

Primary Defenses

  • Follow Check-Effects-Interactions pattern.
  • Use nonReentrant modifiers via ReentrancyGuard from OpenZeppelin.

Additional Safeguards

  • Minimize use of external calls, especially to unknown or user-provided addresses.
  • Avoid using call() unless absolutely necessary; transfer() and send() are safer (pre-2300 gas change).

Detection Methods

  • Static analysis for functions that call external addresses before state mutation.
  • Use Slitherโ€™s reentrancy-vulnerabilities detector.
  • Manual audit of withdrawal and callback flows.

๐Ÿ•ฐ๏ธ Historical Exploits

  • Name: Rari Capital Fuse Pool Exploit
  • Date: 2022-04-30
  • Loss: Over $80 million
  • Post-mortem: Link to post-mortem

๐Ÿ“š Further Reading


โœ… Vulnerability Report

id: LS09C
title: Classic Reentrancy
severity: C
score:
impact: 5 
exploitability: 5 
reachability: 5 
complexity: 2  
detectability: 4  
finalScore: 4.6

๐Ÿ“„ Justifications & Analysis

  • Impact: Contract funds can be completely drained by a single attacker.
  • Exploitability: Straightforward with a malicious contract and a vulnerable flow.
  • Reachability: Found in many legacy contracts and newer unguarded withdrawal logic.
  • Complexity: Moderate; attacker just needs fallback function and balance.
  • Detectability: Highโ€”readily caught by tools and basic audit checklists.