Reentrancy Attacks: The $60 Million Vulnerability That Still Haunts DeFi
How reentrancy attacks work, why they remain a persistent threat, and how to prevent them.
Introduction
Reentrancy attacks are a persistent vulnerability in smart contract security. The DAO hack in 2016 resulted in $60 million in losses. In 2024, reentrancy vulnerabilities accounted for approximately $35.7 million in losses according to OWASP Smart Contract Top 10 data, with the Penpie DeFi protocol losing $27 million in a single attack.
How Reentrancy Attacks Work
A reentrancy attack occurs when a malicious contract exploits the order of operations in a vulnerable contract. The attack takes advantage of external calls made before state updates are complete. Here's the typical attack pattern:
// Vulnerable Contract
contract VulnerableVault {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
// DANGER: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// State update happens AFTER external call
balances[msg.sender] -= amount;
}
}
// Attacker Contract
contract Attacker {
VulnerableVault public vault;
function attack() external payable {
vault.deposit{value: msg.value}();
vault.withdraw(msg.value);
}
receive() external payable {
// Re-enter before balance is updated
if (address(vault).balance >= msg.value) {
vault.withdraw(msg.value);
}
}
}
When the attacker calls withdraw(), the vulnerable contract sends ETH before updating the balance. The attacker's receive() function is triggered, which immediately calls withdraw() again. Since the balance hasn't been updated yet, the check passes, and the cycle repeats until the vault is drained.
Types of Reentrancy
Single-Function Reentrancy: The attacker re-enters the same function that made the external call, as shown in the example above.
Cross-Function Reentrancy: The attacker calls a different function that shares state with the vulnerable function. For example, calling a transfer() function while in the middle of a withdraw() call.
Cross-Contract Reentrancy: The attack spans multiple contracts that share state or trust relationships. This is increasingly common in complex DeFi protocols with multiple interacting contracts.
Read-Only Reentrancy: A newer variant where the attacker exploits view functions that read stale state during a callback, leading to incorrect calculations in other protocols that depend on these values.
Real-World Impact in 2024
The Penpie exploit in September 2024 demonstrated that even modern protocols remain vulnerable. Attackers used a sophisticated reentrancy attack combined with flash loans to drain $27 million in ETH. The attack exploited a callback mechanism in the staking rewards distribution, allowing repeated claims before the protocol could update the user's reward state.
Mitigation Strategies
1. Checks-Effects-Interactions Pattern
Always follow this order: perform all checks first, update all state variables, then make external calls last.
function withdraw(uint256 amount) external {
// 1. CHECKS
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. EFFECTS - Update state BEFORE external call
balances[msg.sender] -= amount;
// 3. INTERACTIONS - External call LAST
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
2. Reentrancy Guards
Use OpenZeppelin's ReentrancyGuard or implement your own mutex lock:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}
3. Pull Over Push Pattern
Instead of sending funds directly, let users withdraw their funds. This puts the external call in the user's control and limits attack surface.
Best Practices
- Always use reentrancy guards on functions that make external calls
- Follow the checks-effects-interactions pattern religiously
- Be especially careful with callbacks and hook functions
- Consider read-only reentrancy in view functions
- Use static analysis tools like Slither to detect potential vulnerabilities
- Have your contracts audited by experienced security researchers
Conclusion
Reentrancy attacks remain a critical threat to smart contract security despite being well-understood. The combination of large TVL in DeFi protocols and the irreversible nature of blockchain transactions makes proper protection essential. By following established patterns, using battle-tested libraries, and conducting thorough audits, developers can significantly reduce the risk of falling victim to these devastating attacks.