Crypto Training

Reentrancy in 2026: Callback Surfaces You Still Miss

Reentrancy is not just withdraw-before-update. Token standard callbacks, cross-function paths, and read-only windows still create severe failures.

Crypto Training2025-12-292 min read

“Use CEI” is still correct and still incomplete.

Modern incidents often come from callback surfaces teams did not model as callbacks.

flowchart TD U[User action] --> T[Token / hook interaction] T --> C[External callback] C --> X[Unexpected reentry path] X --> S[State observed mid-transition] S --> L[Loss or liveness failure]

Reentrancy surface map#

SurfaceWhy teams miss itTypical impact
ERC-777 hooks“We only support ERC-20” assumptionsCross-function reentry
ERC-721 safe callbacksTreated like plain transferInventory/accounting mismatch
ERC-1155 batch callbacksComplex multi-asset flowsPartial state mutation abuse
Read-only reentry“view means safe” thinkingOracle/state desync exploit

A realistic cross-function bug#

SOLIDITY
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Pool {
    mapping(address => uint256) public credit;

    function deposit() external payable {
        credit[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(credit[msg.sender] >= amount, "insufficient");
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
        credit[msg.sender] -= amount;
    }

    function migrate(address to, uint256 amount) external {
        // unguarded sibling function
        require(credit[msg.sender] >= amount, "insufficient");
        credit[msg.sender] -= amount;
        credit[to] += amount;
    }
}

Even if withdraw gets guarded later, migrate may still be reachable during callback chains if not phase-gated.

Phase-gated reentrancy defense#

stateDiagram-v2 [*] --> Idle Idle --> Mutating: enter user function Mutating --> External: performing external call External --> Mutating: callback returns Mutating --> Idle: finalize External --> Blocked: sibling state function called Blocked --> External: revert

Treat reentrancy as state-machine correctness, not a decorator checkbox.

Read-only reentrancy is operationally dangerous#

If a protocol reads your view function while your state is mid-update, that protocol may act on inconsistent data.

This can break:

  • collateral checks
  • liquidation eligibility
  • price guards

Mitigation patterns:

  1. publish only finalized state from views.
  2. split internal transient values from exported values.
  3. avoid exposing half-settled accumulators.

Test strategy that actually catches variants#

Use malicious receiver contracts for each integration surface.

SOLIDITY
function test_ReenterViaERC777Hook() public {
    attacker.armHook();
    vm.expectRevert();
    victim.depositAndStake(address(attacker), 100e18);
}

function invariant_NoNegativeAccountingAfterCallbacks() public {
    assertGe(victim.totalAssets(), victim.totalLiabilities());
}

Practical audit checklist#

  • List every external call (token, router, hook, oracle).
  • Enumerate every function reachable from callback context.
  • Verify sibling mutators share the same phase lock.
  • Assert post-callback invariants in tests.

Further reading#