Cross-Function Reentrancy Exploit Generator
Sigvex exploit generator that validates cross-function reentrancy vulnerabilities where shared state is inconsistent across multiple functions during an external call.
Cross-Function Reentrancy Exploit Generator
Overview
The cross-function reentrancy exploit generator validates vulnerabilities where an external call in Function A creates a window during which Function B — which shares state with Function A — can be called with a stale view of that state. Unlike simple reentrancy (which re-enters the same function), this variant exploits the inconsistency between two functions that lack a shared reentrancy lock.
This pattern is historically linked to the Cream Finance exploit (October 2021, $130M) and the Lendf.Me hack (2020, $25M), both of which involved ERC-777 token callbacks enabling callers to invoke different functions while the initial call was in progress.
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
- Setup: The attacker deposits collateral into a lending contract. The contract records the balance in storage slot 0 and a derived borrow limit in slot 1. No global reentrancy guard is set (slot 2 = 0).
- Trigger (Function A): The attacker calls
withdrawCollateral(amount). The function validates the balance, sends ETH (external call), and schedules the state update for after the call returns. - Exploitation (Function B): During the external call’s callback (the attacker’s
receive()), the attacker callsborrow(limit). Because Function A has not yet updated slot 0 or slot 1,borrow()reads the pre-withdrawal collateral and borrow limit. The attacker borrows against collateral they are simultaneously withdrawing. - Impact: The attacker receives both the withdrawn collateral and the borrowed funds. The protocol has disbursed more value than the attacker’s collateral actually backs.
The generator runs four execution scenarios:
| Scenario | Description | Storage slot 2 (guard) | Slot 3 (state-before-call) |
|---|---|---|---|
| 1 | Normal withdraw (victim caller) | 0 (no guard) | 1 (state updated) |
| 2 | Cross-reentrant (attacker) | 0 (no guard) | 0 (NOT updated) |
| 3 | Cross-function call via transfer() | 0 (no guard) | 0 (NOT updated) |
| 4 | Protected (global guard) | 1 (guard set) | 1 (updated) |
If Scenario 1 succeeds and either Scenario 2 or Scenario 3 also succeeds, the generator reports a critical cross-function reentrancy finding with confidence 0.90.
Exploit Mechanics
Sigvex constructs the test by executing the contract bytecode under four world-state configurations. Key implementation details:
- Selector extraction: The 4-byte selector is read from
finding.locations[0].bytecode_offset. The fallback selector is0x3ccfd60b(withdraw()). - ERC-20 transfer selector: Scenario 3 uses the ERC-20
transfer()selector (0xa9059cbb) to simulate calling a different state-modifying function during the callback. - Storage layout: Slot 0 = user balance, Slot 1 = total supply, Slot 2 = reentrancy guard flag, Slot 3 = whether state was pre-updated.
- Verdict logic: The finding is critical if Scenario 1 does not revert and at least one of Scenarios 2–3 does not revert.
The generated PoC demonstrates both the vulnerable pattern and the correct nonReentrant fix:
// VULNERABLE: Functions A and B share state but not a reentrancy guard
contract VulnerableLendingProtocol {
mapping(address => uint256) public deposits;
mapping(address => uint256) public borrowLimit;
// Function A: sends ETH before updating state
function withdrawCollateral(uint256 amount) external {
require(deposits[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}(""); // callback window
require(success);
deposits[msg.sender] -= amount;
borrowLimit[msg.sender] = deposits[msg.sender] * 75 / 100;
}
// Function B: reads stale state during Function A's callback
function borrow(uint256 amount) external {
require(amount <= borrowLimit[msg.sender]); // stale borrow limit!
borrowLimit[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Remediation
- Detector: Cross-Function Reentrancy Detector
- Remediation Guide: Cross-Function Reentrancy Remediation
Apply nonReentrant from OpenZeppelin’s ReentrancyGuard to all state-changing functions that share state. A per-function guard is insufficient — the guard must be global across the entire contract:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
function withdraw(uint256 amount) external nonReentrant { ... }
function transfer(address to, uint256 amount) external nonReentrant { ... }
// Both functions share the same _status flag
}
References
- Cream Finance Hack Analysis (October 2021, $130M)
- Lendf.Me Hack Analysis (April 2020, $25M)
- SWC-107: Reentrancy
- OpenZeppelin ReentrancyGuard Documentation