पाठ 16 / 25
Reentrancy and Checks-Effects-Interactions
Recognise reentrancy and prevent it with CEI and guards.
The classic smart contract bug
Reentrancy happens when a contract makes an external call (for example, sending ether) before updating its own state, and the recipient's code calls back into the original function while the old state is still in place. The 2016 DAO hack drained millions of ether this way: the attacker's receive function called withdraw again and again before the balance was set to zero. The core defence is the checks-effects-interactions (CEI) pattern: first check conditions, then apply effects (update balances and state), and only then perform interactions (external calls and transfers). Add ReentrancyGuard (nonReentrant modifier) on functions that make external calls, especially when several functions share state, because cross-function and read-only reentrancy (a view function returning stale state during a callback, which another protocol trusts) are subtler variants. Token standards with callbacks (ERC-777, ERC-721 safeTransferFrom, ERC-1155) can also trigger reentrancy. Treat every external call as handing control to an attacker.
Checks, effects, interactions
Update state before calling out, so a callback sees the new state.
A vulnerable vault and the fixed version
The order of state update and external call is the whole difference.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() external payable { balances[msg.sender] += msg.value; }
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTION first: attacker re-enters here
require(ok, "send failed");
balances[msg.sender] = 0; // EFFECT too late
}
}
contract Vault is ReentrancyGuard {
mapping(address => uint256) public balances;
error InsufficientBalance(uint256 available, uint256 requested);
error TransferFailed();
event Deposited(address indexed account, uint256 amount);
event Withdrawn(address indexed account, uint256 amount);
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint256 amount) external nonReentrant {
uint256 bal = balances[msg.sender]; // CHECKS
if (amount > bal) revert InsufficientBalance(bal, amount);
balances[msg.sender] = bal - amount; // EFFECTS
(bool ok, ) = msg.sender.call{value: amount}(""); // INTERACTIONS
if (!ok) revert TransferFailed();
emit Withdrawn(msg.sender, amount);
}
}CEI first, guard second
nonReentrant is a safety net, not a substitute for correct ordering. Apply CEI everywhere, and add the guard on functions that make external calls or share state with ones that do.
त्वरित जाँच: In the checks-effects-interactions pattern, when should external calls happen?
- First
- Before checking inputs
- After state has been updated
- In the constructor only
Answer
After state has been updated — Interactions come last so that re-entrant calls see updated state.