The Four Faces of Reentrancy
Reentrancy remains one of the most misclassified vulnerabilities in smart contracts. Since the famous TheDAO hack (2016), the EVM has evolved — but so have the variants. This article breaks down the four distinct types of reentrancy every EVM developer must understand.
1. Single‑Function Reentrancy
The classic case: a function makes an external call before updating state, and the callee re‑enters the same function.
// ❌ VULNERABLE
pragma solidity ^0.8.0;
contract EtherStore {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0);
// external call BEFORE state update
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0; // too late
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
withdraw() → receives ether → fallback() calls withdraw() again → state not yet zeroed → drain.
Mitigation: Checks-Effects-Interactions pattern, or use a reentrancy guard (e.g., OpenZeppelin's ReentrancyGuard).
2. Cross‑Function Reentrancy
The attacker re‑enters a different function of the same contract that shares the same inconsistent state.
// ❌ VULNERABLE
contract CrossFunction {
mapping(address => uint256) public credits;
bool public locked;
function redeem() external {
require(!locked);
uint256 amount = credits[msg.sender];
require(amount > 0);
locked = true;
(bool ok, ) = msg.sender.call{value: amount}(""); // re‑enters borrow()
require(ok);
credits[msg.sender] = 0;
locked = false;
}
function borrow() external {
require(!locked);
// uses credits[] as collateral — but redeem() hasn't updated it yet
uint256 loan = credits[msg.sender] / 2;
credits[msg.sender] -= loan; // underflow if not careful
// ...
}
}
Key insight: The attacker calls redeem(), then during the external call, calls borrow() which reads the still‑non‑zero credits[msg.sender].
Mitigation: Apply a mutex (reentrancy guard) across all external functions that touch the same state.
3. Cross‑Contract Reentrancy
The re‑entrancy happens across two different contracts that share state – often via a callback or a proxy.
// Contract A (pool)
function withdraw() external {
uint256 amt = balances[msg.sender];
// calls B.getReward()
(bool ok, ) = msg.sender.call{value: amt}("");
require(ok);
balances[msg.sender] = 0;
}
// Contract B (reward manager)
function getReward() external view returns (uint256) {
// reads A.balances[msg.sender] — still non‑zero during reentrancy
return A.balances(msg.sender) / 10;
}
Real‑world example: Uniswap V1 ETH‑>token swap used a callback; a malicious token contract could re‑enter the swap function before the ETH transfer was finalised.
Mitigation: Avoid cross‑contract calls that assume external state consistency. Use a guard that covers the entire contract boundary.
4. Read‑Only Reentrancy
The most subtle variant. The attacker does not modify state but re‑enters a view / pure function that returns a value used by a third party (oracle, liquidation, accounting).
// ❌ VULNERABLE — read-only reentrancy
contract Vault {
uint256 public totalAssets;
mapping(address => uint256) public shares;
function redeem(uint256 sharesToBurn) external returns (uint256) {
uint256 assets = (sharesToBurn * totalAssets) / totalSupply();
// external call before state update
(bool ok, ) = msg.sender.call{value: assets}("");
require(ok);
_burn(msg.sender, sharesToBurn);
}
// view function returns inflated value during reentrancy
function totalAssets() public view returns (uint256) {
return address(this).balance; // still includes the *not yet burned* shares
}
}
Impact: A liquidator or protocol that uses totalAssets() as a price oracle will see a manipulated value during the reentrancy window.
Mitigation: Never use address(this).balance or similar mutable state inside view functions that are used for critical accounting. Apply a reentrancy guard even on view calls if they are used in a sensitive context.
Summary Table
| Type | Re‑enters same function? | Re‑enters different function? | Cross‑contract? | Read‑only? |
|--------------------|--------------------------|-------------------------------|-----------------|------------|
| Single‑function | ✅ | ❌ | ❌ | ❌ |
| Cross‑function | ❌ | ✅ | ❌ | ❌ |
| Cross‑contract | possible | possible | ✅ | ❌ |
| Read‑only | ❌ | ❌ | ❌ | ✅ |
Always ask: “Can an external call before state update lead to a re‑entry that sees stale data?” If yes, protect it.
← Back to Research