Cross-Function Reentrancy Remediation
How to prevent cross-function reentrancy by applying the Checks-Effects-Interactions pattern and reentrancy guards to all functions sharing the same state.
Cross-Function Reentrancy Remediation
Overview
Cross-function reentrancy occurs when an external call in function A allows an attacker to re-enter function B before A has finished updating its state. Both functions share a mutable storage variable (e.g., a balances mapping), and function B reads that variable while function A has not yet committed its update.
Related Detector: Cross-Function Reentrancy
The remediation requires one of two approaches: apply Checks-Effects-Interactions (CEI) to function A so its state update happens before the external call, or apply nonReentrant guards to every function that reads the shared state.
Recommended Fix
Before (Vulnerable)
// State update AFTER external call — reentrancy window
contract VulnerableProtocol {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
// External call before state update — window for re-entry into borrowAgainst()
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // Too late
}
function borrowAgainst(uint256 loanAmount) external {
// Reads stale balance during withdraw()'s external call
require(balances[msg.sender] >= loanAmount);
_issueLoan(msg.sender, loanAmount);
}
}
After (Fixed)
// Option 1: Checks-Effects-Interactions on withdraw()
contract SafeProtocol {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Effect BEFORE interaction
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
function borrowAgainst(uint256 loanAmount) external {
// Now reads updated balance — no exploitable window
require(balances[msg.sender] >= loanAmount);
_issueLoan(msg.sender, loanAmount);
}
}
CEI alone is sufficient when the external call and the shared-state functions are logically separated. If CEI cannot be fully applied (e.g., the state depends on the call result), use nonReentrant on all functions that share the state:
// Option 2: nonReentrant on ALL functions sharing the same state
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SafeProtocol is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
function borrowAgainst(uint256 loanAmount) external nonReentrant {
require(balances[msg.sender] >= loanAmount);
_issueLoan(msg.sender, loanAmount);
}
}
Alternative Mitigations
Transient storage mutex (EIP-1153): On chains supporting EIP-1153, use transient storage for the reentrancy lock. This is cheaper than a regular SSTORE because transient storage is cleared after each transaction.
// EIP-1153 transient reentrancy guard (Solidity 0.8.24+)
contract TransientGuard {
uint256 private constant REENTRANCY_SLOT = 0x01;
modifier transientNonReentrant() {
assembly { if tload(REENTRANCY_SLOT) { revert(0, 0) } tstore(REENTRANCY_SLOT, 1) }
_;
assembly { tstore(REENTRANCY_SLOT, 0) }
}
}
Common Mistakes
Applying nonReentrant only to the function that makes the external call — the ReentrancyGuard mutex only prevents re-entry into guarded functions. If borrowAgainst() is not guarded, an attacker can re-enter it during withdraw()’s external call even though withdraw() is guarded.
Assuming CEI on one function is sufficient — if withdraw() applies CEI but borrowAgainst() is called during the external call window and reads the same storage, the updated value from CEI on withdraw() still needs to be visible before the call. Verify that all state updates happen before any external call in the attack path.