FKHRLabs
Blog / Reentrancy in 2025: It's Not Dead — It's Evolved
Security

February 28, 2025 · Read time: 7 min

Reentrancy in 2025: It's Not Dead — It's Evolved

Everyone knows the classic attack. Modern variants — read-only reentrancy, cross-function, cross-contract — are still catching auditors off guard.

The Classic, Briefly

Classic reentrancy is simple: contract A calls contract B, and B calls back into A before A updates its state. The result is stale accounting and, often, drained funds. The fix is known: Checks-Effects-Interactions, a reentrancy guard, and careful external call boundaries.

So why are reentrancy attacks still happening in 2024 and 2025? Because the attack surface has expanded far beyond the classic pattern. Modern protocols compose across vaults, oracles, bridges, routers, hooks and liquid staking derivatives.

Read-Only Reentrancy

This variant does not drain a contract directly. Instead, it manipulates the state another contract reads, without triggering the target's reentrancy guard. The attacker reads temporarily inconsistent state and uses that value in a downstream protocol that trusts it as price, collateral or liquidity.

Read-only reentrancy does not modify the victim contract's state, so standard guards may not help. If your view function can become a price source, treat it like part of your security perimeter.

Cross-Contract Reentrancy

When two protocols are composable, an attacker can create a reentrant loop that spans both. Neither protocol is individually vulnerable in isolation. The vulnerability appears in the composition.

Audit the call graph, not just the contract file. If A calls B, B calls C, and C can call back into A through a different function, direct nonReentrant coverage may not be enough.

function withdraw() external nonReentrant {
    uint256 balance = balances[msg.sender];
    balances[msg.sender] = 0;
    token.safeTransfer(msg.sender, balance);
}

How to Defend

Use ERC-20 tokens, not ERC-777, in DeFi contracts unless you have a strong reason and explicit hook handling. Add guards to view functions that return values used downstream as prices or collateral. Audit your entire call graph, not just individual contracts.

Use Checks-Effects-Interactions strictly, even when you think it is unnecessary. In fork tests, specifically test scenarios where external calls include callbacks. Static analysis catches a portion of variants; targeted adversarial tests catch the rest.

Need help building this?