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
- Attacker deposits 1 ETH into VulnerableVault.
- Attacker calls withdraw(). During the call, their fallback function is triggered.
- In the fallback, attacker recursively calls withdraw() again.
- Since balances[msg.sender] has not yet been set to 0, they withdraw again.
- 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
- SWC-107: Reentrancy
- Solidity Security Patterns: Checks-Effects-Interactions
- OpenZeppelin ReentrancyGuard
- The DAO Incident Timeline
โ 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.