Token Hook Reentrancy
Detects reentrancy through ERC-777, ERC-1155, and ERC-4626 token callback hooks where state updates occur after hook-triggering transfers.
Token Hook Reentrancy
Overview
Remediation Guide: How to Fix Token Hook Reentrancy
The token hook reentrancy detector identifies functions that transfer ERC-777, ERC-1155, or ERC-4626 tokens before updating state, creating a reentrancy window through the token standard’s mandatory callback hooks. Unlike plain ETH transfers, these token standards require the recipient contract to implement callback functions (tokensReceived, onERC1155Received, etc.) that execute during the transfer — giving an attacker the ability to re-enter the calling contract.
Sigvex identifies calls to known hook-triggering functions (send, operatorSend, safeTransferFrom, safeBatchTransferFrom, deposit, withdraw) and checks whether SSTORE operations that update balances or guards occur after these calls in the control-flow ordering.
Why This Is an Issue
Token callback hooks turn every token transfer into a potential reentrancy vector. The Lendf.Me exploit ($25M, April 2020) used ERC-777’s tokensReceived hook to re-enter a lending protocol’s supply function before the balance update completed, draining the pool. ERC-1155’s onERC1155Received creates the same pattern for NFT and multi-token contracts.
ERC-4626 vaults that use ERC-777 as their underlying asset face a compound risk: the vault’s deposit() or withdraw() triggers a token transfer, which triggers a callback, which allows the attacker to re-enter with stale share-to-asset exchange rates.
How to Resolve
// Before: Vulnerable — ERC-777 transfer before state update
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
erc777Token.send(msg.sender, amount, ""); // Hook fires HERE
balances[msg.sender] -= amount; // Too late — attacker already re-entered
}
// After: Fixed — update state before transfer (CEI pattern)
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // State update FIRST
erc777Token.send(msg.sender, amount, ""); // Hook fires after state is consistent
}
Examples
Vulnerable Code
contract VulnerableVault {
IERC777 public token;
mapping(address => uint256) public deposits;
function withdraw(uint256 amount) external {
require(deposits[msg.sender] >= amount, "Insufficient");
// VULNERABLE: ERC-777 send triggers tokensReceived callback
// Attacker re-enters withdraw() with stale deposits balance
token.send(msg.sender, amount, "");
deposits[msg.sender] -= amount;
}
}
Fixed Code
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SafeVault is ReentrancyGuard {
IERC777 public token;
mapping(address => uint256) public deposits;
function withdraw(uint256 amount) external nonReentrant {
require(deposits[msg.sender] >= amount, "Insufficient");
deposits[msg.sender] -= amount; // Effects before interactions
token.send(msg.sender, amount, "");
}
}
Sample Sigvex Output
{
"detector_id": "token-hook-reentrancy",
"severity": "critical",
"confidence": 0.90,
"description": "ERC-777 send() call at offset 0x9c triggers a tokensReceived callback before the balance SSTORE at offset 0xc0. An attacker can re-enter through the callback to drain funds.",
"location": { "function": "withdraw(uint256)", "offset": 156 }
}
Detection Methodology
- Hook-triggering call identification: Matches external calls against known function selectors for ERC-777 (
send,operatorSend,burn), ERC-1155 (safeTransferFrom,safeBatchTransferFrom), and ERC-4626 (deposit,withdraw,redeem). - State update ordering: For each identified hook call, checks whether any
SSTOREthat modifies balance or guard variables occurs after the call in the CFG topological order. - Guard detection: Checks for reentrancy mutex patterns (
nonReentrantguard) that protect the function regardless of call ordering. - Cross-function analysis: Identifies cases where the hook callback can reach a different function that reads stale state (cross-function token hook reentrancy).
Limitations
False positives:
- Contracts that wrap ERC-777 tokens in a non-hook-triggering interface (e.g., using
transferinstead ofsend) may be flagged if the detector cannot distinguish the call target’s token standard. - Contracts with global reentrancy guards may be flagged if the guard pattern is not detected in the bytecode.
False negatives:
- Custom token implementations with non-standard callback mechanisms are not detected.
- Indirect hook triggers through router contracts or aggregators may not be traced.
Related Detectors
- Reentrancy — detects classic ETH-transfer reentrancy
- Cross-Function Reentrancy — detects reentrancy pivoting through multiple functions
- Read-Only Reentrancy — detects reentrancy exploiting view functions