Cross-function reentrancy is a security vulnerability in smart contracts where an attacker's malicious contract, during a callback from a vulnerable function, calls a different function in the same victim contract before the initial function's state changes are finalized. This exploits the fact that the contract's global state is temporarily inconsistent—funds may be credited but balances not yet deducted, or access controls not yet applied—allowing the attacker to bypass intended logic. It is a more subtle variant of the classic reentrancy attack, which typically re-enters the same function.
Cross-Function Reentrancy
What is Cross-Function Reentrancy?
A sophisticated smart contract attack where an external contract exploits state inconsistencies between different functions within the same protocol.
The attack relies on the order of operations within a contract. A common pattern involves a function A that performs a state-changing operation (e.g., sending funds) before it updates a critical internal state variable (e.g., deducting the user's balance). If the recipient of the funds is a malicious contract, its receive or fallback function can call a separate function B in the victim contract, which reads the not-yet-updated state. For example, function B might check the attacker's balance, see the old, inflated amount, and permit an unauthorized withdrawal or privilege escalation.
Mitigating cross-function reentrancy requires the Checks-Effects-Interactions (CEI) pattern as a foundational practice. This mandates that code should first perform all checks (e.g., validating inputs and balances), then apply all effects (updating state variables), and only finally execute external interactions (like call.value() or token transfers). By finalizing all state changes before any external call, the contract eliminates the inconsistent state window that attackers exploit. Developers should apply this pattern consistently across all functions that share state.
Additional defensive measures include using reentrancy guards (like OpenZeppelin's ReentrancyGuard modifier), which place a lock on a function or a set of functions, preventing any re-entrant calls for the duration of the execution. For complex protocols, formal invariant testing and static analysis tools are essential to detect unexpected state dependencies between functions. It is critical to audit not just individual functions but the interactions between all public and external functions within a contract's state machine.
How Cross-Function Reentrancy Works
An explanation of a sophisticated smart contract attack where an external call triggers a different, unexpected function within the same contract.
Cross-function reentrancy is a smart contract vulnerability where a malicious contract exploits a state-changing external call to re-enter the victim contract, but into a different function that relies on the same, not-yet-updated state variable. Unlike classic reentrancy, which re-calls the same function (e.g., withdraw), this attack pivots to a separate function (e.g., transfer) that shares a critical dependency, such as a user's balance. The attacker's goal is to manipulate the intermediate, inconsistent state before the initial function's state updates are finalized, allowing for double-spending or unauthorized actions. This is a form of reentrancy that bypasses simple checks on a single function's re-entry.
The attack flow typically follows a specific pattern. First, the attacker's contract calls a vulnerable function Function A in the victim contract, which performs an external call (e.g., sending Ether) to the attacker before updating a critical state variable like balances[msg.sender]. Upon receiving the Ether, the attacker's fallback or receive function is executed. Instead of calling Function A again, it calls Function B, which also reads the same, stale balances mapping. Because the balance has not yet been deducted from the initial call, Function B executes based on incorrect, inflated data, allowing the attacker to, for instance, transfer tokens they no longer rightfully own.
This vulnerability arises from violating the checks-effects-interactions pattern. The correct pattern mandates: first, perform all checks (e.g., balance sufficient), then apply all state effects (e.g., deduct the balance), and only finally make external interactions (e.g., call.value()). Placing the external call before the state update creates a dangerous window where the contract's internal accounting is out of sync with reality. Functions that share global state—like a single balances mapping used for both withdrawals and transfers—are particularly susceptible to this cross-function exploitation.
Mitigating cross-function reentrancy requires the same fundamental defenses as other reentrancy attacks, but with careful consideration of all state-sharing functions. The primary defense is strict adherence to the checks-effects-interactions pattern. Additionally, using reentrancy guards (like OpenZeppelin's ReentrancyGuard modifier) on all functions that modify shared state can provide a robust safety net. For more complex systems, adopting the pull-over-push pattern for payments, where users withdraw funds themselves, eliminates hazardous external calls from the contract's core logic entirely.
Key Characteristics
Cross-function reentrancy is a specific type of smart contract vulnerability where an external contract exploits the interaction between two or more functions within a victim contract, bypassing intended state checks and security invariants.
The Core Mechanism
The attack exploits the separation of state updates and value transfers across different functions. An attacker's fallback or receive function is called during a transfer (e.g., via a call.value()), allowing them to re-enter the victim contract and call a different function before the original function's state changes are finalized. This violates checks-effects-interactions pattern.
Classic Example: Shared State
A contract with separate withdraw() and transfer() functions that share a balance mapping is vulnerable. An attacker can:
- Call
withdraw()to initiate a transfer. - In the receiving fallback, call
transfer()to send tokens to another address. - The
transferfunction checks the attacker's balance, which hasn't been decremented yet by the originalwithdrawcall, allowing a double-spend.
Distinct from Single-Function Reentrancy
Unlike the classic DAO-style attack, which re-enters the same function, cross-function reentrancy targets interdependent functions. It is more subtle because the vulnerable functions may not individually contain a callback, but their combined logic creates a reentrancy path. This makes it harder to detect with static analysis tools focused on single-function flows.
Superior Defense: CEI Pattern
The most robust mitigation is strict adherence to the Checks-Effects-Interactions pattern:
- Checks: Validate all conditions and inputs.
- Effects: Update all internal state variables.
- Interactions: Perform external calls (e.g., token transfers) last. This order ensures the contract's state is in a consistent and updated condition before any external, potentially malicious, code is executed.
Related Vulnerability: Read-Only Reentrancy
A more advanced variant where a reentrant call doesn't modify the victim's state but queries it (e.g., calling a price oracle or a balanceOf view function). The queried state may be inconsistent mid-transaction, causing other integrated protocols to make faulty decisions based on temporary, incorrect data.
Code Example & Attack Flow
This section details the technical execution of a cross-function reentrancy attack, tracing the malicious transaction flow and its impact on contract state.
The attack flow begins when an attacker-controlled contract calls a vulnerable function, such as withdraw(), in the target contract. This function performs a state-changing operation, like transferring tokens via a low-level call, before updating its internal accounting state (e.g., zeroing the user's balance). The critical vulnerability is the state update lag, where the contract's balance ledger remains unchanged until after the external call completes.
Upon receiving the funds, the attacker's fallback function or receive() function is automatically invoked. This malicious callback then re-enters the target contract, but not through the same withdraw() function. Instead, it calls a different, state-dependent function—such as transfer() or a second withdraw()—that relies on the same, yet-to-be-updated balance. Because the original withdrawal's state change is pending, the contract incorrectly validates the attacker's balance a second time, allowing duplicate or unauthorized operations.
This cycle can repeat multiple times within a single transaction, draining assets in a loop until gas limits are reached or a secondary condition fails. The attack exploits the interdependence of functions sharing state, a pattern common in contracts that separate logic (e.g., a token transfer) from accounting. Defensive patterns like the Checks-Effects-Interactions pattern are designed to prevent this by mandating that all state updates (effects) occur before any external calls (interactions).
Historical Examples & Case Studies
These case studies demonstrate how cross-function reentrancy exploits have been executed in the wild, leading to significant financial losses and shaping modern smart contract security practices.
The CREAM Finance Exploit (August 2021)
This $18.8 million loss involved the AMP token and its unique pre-transfer hook mechanism. The attacker:
- Used a flash loan to supply AMP as collateral.
- Borrowed other assets.
- The AMP token's hook allowed a reentrant call to the lending pool's
borrowfunction again before the initial borrow transaction finalized. - This bypassed the protocol's collateral checks, allowing the attacker to borrow far more than their collateral should have permitted. It underscored that non-standard token implementations (ERC-777, ERC-1363) require special consideration in DeFi integrations.
The Fei Protocol Rari Fuse Incident (April 2022)
Exploited a cross-protocol reentrancy vulnerability between Fei Protocol's PCV (Protocol Controlled Value) deposits and Rari Capital's Fuse pools. The attacker:
- Called a function that withdrew FEI from a Fuse pool to a Fei PCV contract.
- Reentered during the token transfer to call a function that minted new FEI, artificially inflating the pool's balance.
- This allowed the attacker to drain approximately $80 million. The root cause was a violation of the Checks-Effects-Interactions pattern across two tightly integrated but separately governed protocols.
General Prevention: The Checks-Effects-Interactions Pattern
The primary defense against all reentrancy, mandated after these exploits. Code must be structured in this strict order:
- Checks: Validate all conditions and inputs (e.g., balances, permissions).
- Effects: Update all internal state variables (e.g., deduct balances, increment counters).
- Interactions: Perform external calls to other contracts or EOAs last.
- This ensures the contract's state is in a consistent and finalized condition before any external call that could trigger a reentrant attack. Modern development frameworks often enforce this pattern.
The Reentrancy Guard (NonReentrant Modifier)
A widely adopted technical mitigation, popularized by OpenZeppelin libraries. It is a mutex lock that prevents a function from being called recursively.
- When a function with the
nonReentrantmodifier is entered, a boolean flag is set totrue. - Any reentrant call to a protected function will fail because the flag is already set.
- The flag is reset to
falseonly after the original function execution fully completes. - While effective for single-contract reentrancy, it does not fully protect against cross-function or cross-protocol reentrancy, which require rigorous application of the Checks-Effects-Interactions pattern.
Post-Mortem Analysis & Industry Impact
These historical cases led to fundamental shifts in smart contract security:
- Audit Focus: Reentrancy, especially cross-protocol, became a top priority for security auditors.
- Standardization Push: Increased caution around integrating tokens with callback hooks (ERC-777, ERC-1363).
- Protocol Design: Modern DeFi protocols now explicitly design for composition risks, implementing delayed effects or using pull-over-push payment patterns for critical operations.
- Developer Education: The Checks-Effects-Interactions pattern is now a foundational teaching point in smart contract development.
Security Considerations & Mitigations
Cross-function reentrancy is a vulnerability where an external call to an untrusted contract allows it to re-enter the calling contract but into a different function, bypassing state checks and invariants. This glossary details its mechanics and defensive patterns.
Core Vulnerability
Cross-function reentrancy occurs when an external call allows an attacker's contract to call back into the vulnerable contract, but into a different function than the original one. This exploits shared state variables that the second function assumes are valid, but which have not yet been finalized by the first. Unlike single-function reentrancy, the attack vector is not direct recursion but lateral movement within the contract's interface.
- Example: A
withdraw()function sends funds before updating a balance, allowing a reentrant call totransfer()which also reads the same, un-updated balance.
The Checks-Effects-Interactions Pattern
The primary defense is the Checks-Effects-Interactions (CEI) pattern. This strict ordering of operations prevents state inconsistencies before any external calls are made.
- Checks: Validate all conditions and inputs (e.g.,
require(balance[msg.sender] >= amount)). - Effects: Update all internal state variables before any interaction (e.g.,
balance[msg.sender] -= amount). - Interactions: Perform external calls last (e.g.,
msg.sender.call{value: amount}("")).
By finalizing state changes first, any reentrant call into another function will read the correct, updated state.
Reentrancy Guard Modifiers
A reentrancy guard is a function modifier that uses a boolean lock (nonReentrant) to prevent any reentrant calls for the duration of a function's execution. This is a robust, generalized solution.
- Implementation: A modifier sets
locked = trueon entry andlocked = falseon exit, reverting if a reentrant call is attempted. - Scope: Modern guards (e.g., OpenZeppelin's
ReentrancyGuard) are often applied to entire functions, protecting against both single-function and cross-function reentrancy. - Consideration: Guards add gas cost and do not replace the need for correct internal logic and CEI where state dependencies are complex.
Pull Over Push Payments
The pull-over-push pattern mitigates reentrancy by shifting the risk of external calls. Instead of a contract "pushing" funds to users (an active external call), users "pull" their owed funds from the contract in a separate transaction.
- Mechanism: Store a record of a user's entitlement (e.g.,
withdrawableBalance[user]). Let the user call awithdraw()function to claim it. - Benefit: The vulnerable external call is initiated by the user, not the core contract logic, removing the reentrancy window from critical state-update functions.
- Use Case: Preferred for batch operations or payments to untrusted addresses.
State Variable Isolation
Preventing cross-function reentrancy requires careful design to isolate state variables or ensure their updates are atomic across related functions. Functions that share critical state must be protected as a group.
- Risk: A
transferFrom()function that shares abalancesmapping withmint()is vulnerable ifmintis called reentrantly beforetransferFromupdates the state. - Mitigation: Apply a reentrancy guard to all functions that read/write the same sensitive state, or refactor to use the CEI pattern rigorously in each.
- Audit Focus: Mappings and global variables are common attack surfaces for this vulnerability.
Real-World Example: The DAO Attack
The 2016 attack on The DAO is the canonical example of reentrancy, demonstrating both single and cross-function variants. The vulnerable splitDAO function performed an external call to send Ether before updating the attacker's internal token balance.
- Cross-Function Vector: The reentrant call was made to the
withdrawRewardForfunction, which relied on the same, not-yet-updated balance state. - Impact: This allowed the attacker to repeatedly drain funds, leading to a loss of 3.6M ETH and the Ethereum network's contentious hard fork.
- Legacy: This event directly led to the standardization of the CEI pattern and reentrancy guards as fundamental security practices.
Comparison: Reentrancy Attack Types
A technical breakdown of the primary reentrancy attack vectors, focusing on their execution patterns and the specific state inconsistencies they exploit.
| Attack Vector | Single-Function | Cross-Function | Cross-Contract |
|---|---|---|---|
Primary Vulnerability | State update after external call | State inconsistency between functions | Trusted external contract |
Exploited State | Balance/allowance of calling contract | Shared state variable(s) across functions | State of a separate, dependent contract |
Call Pattern | A -> A (recursive) | A -> B -> A (inter-function) | A -> B -> C -> A (inter-contract) |
Prevention (Checks-Effects-Interactions) | Effective if followed | Ineffective for shared state | Requires system-wide audit |
Guard Effectiveness | Reentrancy guard blocks attack | Reentrancy guard may be bypassed | Reentrancy guard on A is ineffective |
Complexity to Execute | Low | Medium | High |
Example | TheDAO (2016) | Uniswap/Lendf.me (2020) | Complex DeFi protocol interactions |
Common Misconceptions
Cross-function reentrancy is a subtle and often misunderstood vulnerability where an attacker exploits the shared state between different functions within the same contract, not just recursive calls to the same function. This glossary clarifies the core mechanics, dispels common myths, and outlines critical defense patterns.
Cross-function reentrancy is a smart contract vulnerability where an external call allows an attacker to re-enter the contract and invoke a different function that incorrectly relies on a shared state variable that hasn't yet been updated. It differs from single-function reentrancy, where the attacker re-enters the same function. The classic example involves a contract with a withdraw() function that sends funds before updating a balance, and a separate transfer() function that checks that same balance. An attacker's fallback function, triggered by the withdraw() send, can call transfer() while the balance is still stale, allowing double-spending across functions.
Key Distinction: The vulnerability stems from state dependencies between functions, not just re-execution of the same logic. The Checks-Effects-Interactions pattern must be applied to all functions that share critical state, not just the one making the external call.
Visual Explainer: The Attack Path
This section illustrates the step-by-step sequence of a cross-function reentrancy attack, a sophisticated exploit where an attacker manipulates the order of state changes between different functions in a smart contract.
A cross-function reentrancy attack exploits the logical coupling between two or more functions that share the same vulnerable state variable. Unlike a classic single-function reentrancy, the attacker does not re-enter the same function. Instead, after an initial call to function A triggers a state-changing callback, the malicious contract's fallback function re-enters the vulnerable contract by calling function B. Because function A has not yet updated the shared state (e.g., a user's balance), function B operates on outdated, incorrect data, allowing the attacker to double-spend or drain funds.
The attack path typically follows a specific sequence. First, the attacker calls a function like withdraw() that sends Ether before updating the internal balance. The victim contract performs a low-level call .call{value: amount}("") to the attacker's address. This transfers control to the attacker's fallback or receive function. Crucially, the victim's balance has not yet been decremented. From within this callback, the attacker now calls a second, separate function in the same contract, such as transfer() or another withdraw(), which reads the same, still-unmodified balance. The contract incorrectly authorizes this second operation, as its state is stale.
This exploit is possible because of the lack of a reentrancy guard protecting the shared state and the use of low-level calls that forward all remaining gas. Functions using the Checks-Effects-Interactions (CEI) pattern individually are still vulnerable if the "effects" (state updates) for one function are not completed before an "interaction" allows re-entry into a different function. The vulnerability is often subtler than the classic DAO attack, as it spans multiple entry points, making it harder to detect in code audits that only check individual functions for reentrancy.
Frequently Asked Questions
Cross-function reentrancy is a critical smart contract vulnerability where an attacker exploits the interaction between multiple functions within the same contract to drain funds or manipulate state.
Cross-function reentrancy is a smart contract vulnerability where an attacker exploits the interaction between two or more functions that share the same state, allowing them to re-enter the contract in a malicious sequence before the initial function's state updates are finalized. Unlike classical reentrancy, which targets a single function (e.g., a withdraw), this attack manipulates the contract's internal logic by calling a different, seemingly unrelated function while the original call is still pending. The core issue is a violation of the checks-effects-interactions pattern, where state changes happen after an external call. This can lead to double-spending, incorrect balance calculations, or unauthorized access to funds.
Get In Touch
today.
Our experts will offer a free quote and a 30min call to discuss your project.