Reentrancy Attacks
Reentrancy Attacks
Reentrancy is one of the most critical and dangerous vulnerabilities in smart contracts. It occurs when a function makes an external call to another untrusted contract before resolving its own state.
What is Reentrancy?
A reentrancy attack happens when:
1. Contract A calls Contract B
2. Contract B calls back into Contract A before the first call completes
3. The state in Contract A hasn't been updated yet
4. Contract B exploits this to drain funds or manipulate state
Classic Example: The DAO Hack
The infamous DAO hack in 2016 exploited a reentrancy vulnerability, resulting in the loss of ~$60 million worth of ETH and leading to the Ethereum hard fork.
Vulnerable Pattern
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
// Vulnerable: External call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // State update happens AFTER the call
}Attack Scenario
contract Attacker {
VulnerableContract target;
function attack() external payable {
target.deposit{value: 1 ether}();
target.withdraw(1 ether);
}
// Fallback function that re-enters
receive() external payable {
if (address(target).balance >= 1 ether) {
target.withdraw(1 ether);
}
}
}Prevention Methods
1. Checks-Effects-Interactions Pattern
Always follow this order:
- **Checks**: Validate all conditions
- **Effects**: Update all state variables
- **Interactions**: Make external calls
function withdraw(uint amount) public {
// Checks
require(balances[msg.sender] >= amount);
// Effects (update state FIRST)
balances[msg.sender] -= amount;
// Interactions (external call LAST)
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}2. ReentrancyGuard Modifier
Use OpenZeppelin's ReentrancyGuard:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyContract is ReentrancyGuard {
mapping(address => uint) public balances;
function withdraw(uint amount) public nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}3. Pull Payment Pattern
Instead of pushing payments, let users pull their funds:
mapping(address => uint) public pendingWithdrawals;
function initiateWithdrawal(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
pendingWithdrawals[msg.sender] += amount;
}
function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
require(amount > 0);
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}Best Practices
1. Always update state before external calls
2. Use ReentrancyGuard for critical functions
3. Limit the gas forwarded to external calls
4. Use `.transfer()` or `.send()` instead of `.call{value: }()` when possible
5. Implement withdrawal patterns instead of push payments
6. Test thoroughly with tools like Slither and Echidna
Detection Tools
- **Slither**: Static analysis tool that detects reentrancy
- **Mythril**: Symbolic execution tool
- **Manticore**: Dynamic analysis
- **Foundry Fuzz**: Fuzz testing framework
References
- [Consensys Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/attacks/reentrancy/)
- [SWC-107: Reentrancy](https://swcregistry.io/docs/SWC-107)