5 reentrancy patterns I keep finding in small DeFi vaults
Almost every small vault I review has a nonReentrant modifier on withdraw(). And almost every one still has a reentrancy issue somewhere else. A single guard on the obvious function creates a false sense of safety. Reen
Almost every small vault I review has a nonReentrant modifier on withdraw(). And almost every one still has a reentrancy issue somewhere else.
A single guard on the obvious function creates a false sense of safety. Reentrancy is about state that's inconsistent during an external call β and that happens in more places than the one function everyone remembers to protect. Here are five patterns I keep finding, and how to catch each.
1. Cross-function reentrancy
The classic. withdraw() is guarded, but it makes an external call before updating a shared balance β and during that call the attacker re-enters a different, unguarded function that reads the stale balance.
function withdraw(uint256 amount) external nonReentrant {
(bool ok,) = msg.sender.call{value: amount}(""); // external call first
require(ok);
balances[msg.sender] -= amount; // state updated after
}
function transfer(address to, uint256 amount) external { // NOT guarded
balances[msg.sender] -= amount; // still sees the pre-withdraw balance
balances[to] += amount;
}
A nonReentrant on withdraw doesn't help: the re-entry lands in transfer. Fix: effects before interactions β update balances before the call β and apply the guard to every function that touches shared state, not just one.
2. Read-only reentrancy
The one that bit Curve/Balancer integrators. A view function (say getPrice() or totalAssets()) reads state that's temporarily inconsistent mid-transaction. Your vault calls that view during someone else's external call and gets a manipulated value β even though nothing "wrote" during your call.
Views can't be nonReentrant (they don't write), so the guard doesn't apply. Fix: don't consume an external protocol's price/reserves view without checking whether it can be entered mid-operation; use a reentrancy-aware read (some protocols expose a "check" you can call), or a manipulation-resistant source.
3. ERC-777 / ERC-677 / callback-token hooks
You assumed the token is plain ERC-20. But ERC-777 calls tokensReceived on the recipient, and ERC-677/1363 call hooks on transfer. If your accounting isn't settled before transferFrom/transfer, that hook is a re-entry point into your vault.
Fix: strict checks-effects-interactions even around token transfers, and be explicit about which token standards you support (documented assumptions are spec, not luck).
4. Cross-contract reentrancy
Your vault and your strategy/adapter share state through storage or a shared accounting contract. Vault is guarded, Strategy is guarded β but a call chain Vault β external β Strategy re-enters while the shared state is half-updated. Per-contract guards don't compose across contracts.
Fix: treat the trust boundary as the whole system. Settle shared accounting before any external call anywhere in the chain, and consider a shared reentrancy lock for tightly-coupled contracts.
5. The root cause: CEI violations
Patterns 1β4 are symptoms. The disease is Checks-Effects-Interactions ordering: any external call that happens before the contract's state reflects what just occurred. Once you internalize CEI, most reentrancy disappears β the guard becomes a backstop, not the plan.
A quick self-audit: search your contracts for .call, .transfer, safeTransfer, and any external interface call, and ask for each β "is all my state already updated at this line?" If not, that's a candidate.
Catch them automatically
My free scanner, OpenClaw Audit, has dedicated detectors for cross-function reentrancy and CEI-ordering candidates (and it's calibrated to stay quiet on correct code β Morpho Blue's formally-verified safeTransfers don't trip it). One command:
pipx run --spec git+https://github.com/juan23z/openclaw-audit openclaw-audit <your-repo>
Findings are heuristic candidates β verify each. But it points your eyes at the right lines.
I review small DeFi protocols for exactly these issues β real, exploitable findings only, no padded reports. A hand-verified Quick Scan is $49 (one contract, 48h, and if it's not useful you don't pay).
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.