Unsafe Callback Remediation
Overview
Related Detector: Unsafe Callback
Callbacks — token-transfer hooks, flash-loan executeOperation, generic notifiers — hand control to an external address by design. Without guardrails, the callee can re-enter, revert strategically, or read half-updated state. Unlike accidental reentrancy, the call is intentional, so the fix is not to remove it but to make it safe: finalize state first, guard against reentry, and constrain who can be called back.
Recommended Fix
Before (Vulnerable)
function flashLoan(address receiver, uint256 amount) external {
token.transfer(receiver, amount);
// Callback runs while the loan is outstanding and state is mid-flight.
IReceiver(receiver).executeOperation(amount);
require(token.balanceOf(address(this)) >= reserve, "Not repaid");
}
After (Fixed — reentrancy guard, checks after the hook, effects first)
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "Reentrant");
_locked = 2;
_;
_locked = 1;
}
function flashLoan(address receiver, uint256 amount) external nonReentrant {
uint256 before = token.balanceOf(address(this));
token.transfer(receiver, amount);
IReceiver(receiver).executeOperation(amount);
require(token.balanceOf(address(this)) >= before, "Not repaid"); // verify after callback
}
Where the set of valid callback targets is known, keep an allowlist and reject callbacks from anyone else. Never leave critical invariants unchecked across the hook.
Common Mistakes
- Adding a reentrancy guard but reading a manipulated value (e.g. a pool price) during the callback window elsewhere in the system — see read-only reentrancy.
- Trusting the callback’s return value as authorization instead of verifying the contract’s own invariants afterward.
- Forwarding all gas to an untrusted callback, enabling grief via return bombs or deep call stacks.