Callback State Mutation
Detects state mutations after external calls that may trigger callbacks, violating the Checks-Effects-Interactions pattern.
Callback State Mutation
Overview
The callback state mutation detector identifies functions that update storage state after making external calls. When a contract calls an external address, the recipient may execute a callback into the calling contract (via ERC-777 tokensReceived, ERC-721 onERC721Received, ERC-1155 onERC1155Received, or custom hooks). If the calling contract writes to storage after the callback returns, the callback may have already re-entered and observed stale state.
This pattern is responsible for $500M+ in real-world losses, including the Curve/Vyper reentrancy exploit ($70M+) and multiple DEX/AMM exploits during 2023-2024.
Why This Is an Issue
The Checks-Effects-Interactions (CEI) pattern requires that all state changes happen before external calls. When state updates follow an external call, any callback triggered by that call can re-enter the contract and operate on the pre-update state. For token transfer functions, this means balances can be double-spent.
Unlike traditional reentrancy (which focuses on the re-entrant call itself), this detector targets the ordering violation: the fact that storage writes happen after the point where callbacks execute.
How to Resolve
// Before: Vulnerable -- state update after external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] -= amount; // Too late -- callback already ran
}
// After: Fixed -- state update before external call (CEI)
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Effect first
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
}
Examples
Vulnerable
function transferWithHook(address to, uint256 amount) external {
IERC20(token).transfer(to, amount); // External call -- may trigger callback
userDeposits[msg.sender] -= amount; // State update after callback
emit Transfer(msg.sender, to, amount);
}
Fixed
function transferWithHook(address to, uint256 amount) external nonReentrant {
userDeposits[msg.sender] -= amount; // Effect before interaction
IERC20(token).transfer(to, amount);
emit Transfer(msg.sender, to, amount);
}
Sample Sigvex Output
[HIGH] callback-state-mutation
State mutation after external call in transfer function 'withdraw'
Location: withdraw @ block 2, instruction 5
Confidence: 0.75
Function 'withdraw' performs external call at block 1 (inst 3) followed
by state change at block 2 (inst 5). This violates Checks-Effects-
Interactions (CEI) pattern.
Detection Methodology
- External call identification: Locates
CALLandDELEGATECALLinstructions that may trigger callbacks. - Post-call storage write scanning: Finds
SSTOREoperations that execute after the external call within the same function. - Reentrancy guard detection: Checks for early
SLOADpatterns in the first few instructions that indicate a mutex guard, reducing false positives. - Transfer function prioritization: Functions with names containing
transfer,withdraw,claim,mint, orburnare flagged at higher severity.
Limitations
False positives: Functions that use STATICCALL (read-only) may be grouped with state-changing calls. Functions protected by a reentrancy guard are reported at reduced confidence. False negatives: Indirect callbacks through multi-hop call chains spanning multiple contracts are not traced.
Related Detectors
- Reentrancy — detects classic reentrancy via recursive calls
- Cross-Function Reentrancy — detects reentrancy across different functions
- Read-Only Reentrancy — detects reentrancy via view functions
- Token Hook Reentrancy — detects ERC-777/ERC-1155 callback reentrancy