OpenZeppelin's nonReentrant modifier is the most commonly used reentrancy protection. But slapping it on every function isn't always correct, and it's not always enough. Cross-contract reentrancy, read-only reentrancy, and gas-efficient alternatives with transient storage all require deeper understanding.
How ReentrancyGuard Works
// OpenZeppelin's implementation (simplified)
abstract contract ReentrancyGuard {
uint256 private _status;
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
It's a mutex lock: set a flag on entry, clear it on exit. If a reentrant call hits the flag, it reverts.
What ReentrancyGuard Doesn't Protect Against
1. Cross-Contract Reentrancy
Contract A has a guard. Contract B reads from A during A's callback. A's guard doesn't protect B's reads — B sees stale state.
2. Read-Only Reentrancy
A view function returns incorrect data during a callback because state hasn't been updated yet. The guard doesn't apply to view functions.
3. Cross-Function, Same Contract
If you only apply nonReentrant to withdraw() but not transfer(), an attacker can re-enter via transfer() during withdraw()'s callback.
EIP-1153: Gas-Efficient Guards with Transient Storage
// New in Solidity 0.8.24+ (Cancun upgrade)
// Transient storage is cleared after each transaction — perfect for guards
modifier nonReentrantTransient() {
assembly {
if tload(0) { revert(0, 0) }
tstore(0, 1)
}
_;
assembly {
tstore(0, 0)
}
}
// Gas cost: ~100 gas vs ~5,000 for SSTORE-based guard
Best Practices
- ✅ Apply
nonReentrantto ALL external functions that modify state, not just those with ETH transfers - ✅ Use CEI pattern even WITH a guard — belt and suspenders
- ✅ For cross-contract scenarios, share the guard state or use a global lock
- ✅ Consider EIP-1153 transient storage for gas-efficient guards (Cancun+)
- ✅ Guard
viewfunctions that return sensitive state during callbacks
Reentrancy guards are necessary but not sufficient. A full audit catches the reentrancy paths that a simple modifier misses.