Reentrancy attack is one of the most common and damaging security vulnerabilities in smart contracts. Attackers exploit the characteristic of contracts calling external contracts before state updates to recursively call the target contract and repeatedly extract funds.
Reentrancy Attack Principle
solidity// Vulnerable contract example contract VulnerableBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); // Dangerous: transfer first, update state later (bool success, ) = msg.sender.call{value: amount}(""); require(success); balances[msg.sender] = 0; // State update too late } }
The attacker contract can recursively call withdraw() to extract funds multiple times before the balance is zeroed.
Protection Method 1: Checks-Effects-Interactions Pattern
This is the most recommended protection method, following the order of check first, then update state, and finally interact.
soliditycontract SecureBank { mapping(address => uint256) public balances; function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); // Checks balances[msg.sender] = 0; // Effects: update state first (bool success, ) = msg.sender.call{value: amount}(""); // Interactions require(success, "Transfer failed"); } }
Protection Method 2: Using Reentrancy Guard
OpenZeppelin's ReentrancyGuard is the most commonly used solution.
solidityimport "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract ProtectedBank is ReentrancyGuard { mapping(address => uint256) public balances; function withdraw() public nonReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; } }
Protection Method 3: Using Mutex
Custom implementation of reentrancy lock mechanism:
soliditycontract MutexBank { mapping(address => uint256) public balances; bool private locked; modifier noReentrant() { require(!locked, "Reentrant call detected"); locked = true; _; locked = false; } function withdraw() public noReentrant { uint256 amount = balances[msg.sender]; require(amount > 0, "Insufficient balance"); (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); balances[msg.sender] = 0; } }
Protection Method 4: Using transfer or send
Although transfer and send have Gas limits (2300 Gas) that can prevent reentrancy, they are not recommended because:
- May not be compatible with some smart contract wallets
- Gas limits may change in future Ethereum upgrades
solidity// Not recommended approach function withdraw() public { uint256 amount = balances[msg.sender]; require(amount > 0); balances[msg.sender] = 0; payable(msg.sender).transfer(amount); // Gas limit 2300 }
Best Practices Summary
- Always use Checks-Effects-Interactions pattern: This is the most basic and important protection
- Use OpenZeppelin's ReentrancyGuard: Add an extra layer of protection for complex contracts
- Avoid using call for ETH transfers: If you must use it, ensure the state is updated first
- Code audit: Conduct professional security audits before deployment
- Use static analysis tools: Such as Slither, Mythril, etc. to detect reentrancy vulnerabilities
Tools for Detecting Reentrancy Vulnerabilities
- Slither: Static analysis tool that can detect various vulnerabilities including reentrancy
- Mythril: Symbolic execution tool for analyzing contract security
- Echidna: Fuzzing tool to discover edge cases
- Certora: Formal verification tool for mathematically proving contract correctness