A Complete Guide to Reentrancy Attacks
Dec 11, 2024
6 Minutes
Table of Contents
Introduction to Reentrancy Attacks
Classic Reentrancy Attack
How It Works
Code Example
Variations of Reentrancy Attacks
Cross-Function Reentrancy
Cross-Contract Reentrancy
Read-Only Reentrancy
Conclusion
1. Introduction to Reentrancy Attacks
A reentrancy attack is a common vulnerability in smart contracts. It occurs when an attacker repeatedly calls a function in a smart contract before the initial function call is completed.
In the context of reentrancy attacks, the prevalent association is often with the classic reentrancy attack that arises when the Checks-Effects-Interactions (CEI) pattern is not properly adhered to. This association is understandable, as such scenarios typically highlight a severe vulnerability. Specifically, they can result in unauthorized withdrawal of funds from a contract or unintended modifications to the contract's state, posing significant security risks.
2. Classic Reentrancy Attack
The classic reentrancy attack happens when a malicious contract repeatedly calls back into a vulnerable contract before the original function execution is completed, exploiting the incomplete state of the vulnerable contract. This vulnerability arises because external calls are executed before the contract's state is properly updated. As a result, malicious actors can exploit this sequence to re-enter the contract and manipulate its logic or drain funds, bypassing the intended safeguards.
How It Works
The key to a reentrancy attack lies in exploiting the control flow of a smart contract. It often targets contracts that interact with external entities, such as other contracts or wallets. Here's a step-by-step breakdown:
1. Vulnerable Contract: The victim contract has a function that:
- Sends Ether to a user (e.g.
withdraw
).- Updates the user’s balance after the transfer.
2. Attacker Contract: The attacker creates a malicious contract designed to:
- Receive Ether from the vulnerable contract.
- Call the
withdraw
function of the vulnerable contract recursively before the first invocation completes.
3. Attack Process:
- The attacker deposits some Ether into the vulnerable contract.
- The attacker calls the vulnerable contract’s
withdraw
function.- During the transfer process, the vulnerable contract invokes the fallback function of the attacker's contract.
- The attacker’s fallback function re-invokes the vulnerable contract’s
withdraw
function, draining funds before the original transaction completes.
4. Funds Drained: Because the balance in the vulnerable contract is only updated after the withdrawal, the attacker can withdraw funds repeatedly in a single transaction until the contract is drained.
Code Example
To better illustrate the described reentrancy attack, let us consider the following code example:
While the classic reentrancy attack involving recursive calls is the most well-known, reentrancy variations can have equally or more severe impacts, depending on the specific protocol and its logic. These variations exploit other indirect forms of reentrancy vulnerabilities and can bypass simple protections, potentially causing significant harm.
3. Variations of Reentrancy Attacks
A. Cross-Function Reentrancy Attack
This is a more sophisticated variant of the classic reentrancy attack in smart contracts, where the vulnerability arises not from a single recursive call to the same function, but from the interplay between multiple functions within the same contract. Unlike the traditional reentrancy attack, where a malicious contract repeatedly calls the same function to exploit state inconsistencies (typically with recursive calls), cross-function reentrancy manipulates the contract’s behavior by exploiting the sequence of function calls and the interactions between them.
In a cross-function reentrancy attack, an attacker leverages the dependencies between multiple functions to create a situation where state updates from one function are not fully reflected before another function is called. By carefully timing and ordering function calls, the attacker can exploit inconsistencies in how state is updated or how external calls are made, causing unintended consequences such as fund theft, improper state transitions, or the bypassing of contract logic.
This type of reentrancy attack is harder to detect because it doesn’t rely on the same function being reentered, but rather on a series of function calls that manipulate shared state variables in a sequence. Since the attacker only needs to exploit the interactions between functions (and not recursion within one function), the attack is often subtler and more difficult to identify during audits.
The scenario above can present a tricky challenge for developers and auditors, who may assume that a specific variable state does not need to be updated before making an external call, believing that a classic reentrancy attack cannot be executed within the same function. However, this oversight creates the potential for a cross-function reentrancy attack. In such cases, an external call in one function can trigger a reentrancy attack in another function, allowing malicious actors to manipulate the contract's state in unexpected ways.
How It Works
In a typical contract, multiple functions often interact with shared state variables. For example, one function might handle deposits, another might handle withdrawals, and a third might manage rewards or interest calculations. These functions might not have direct reentrancy protection, especially if they don’t expect to interact with each other in unexpected ways. In a cross-function reentrancy attack, the attacker can exploit these interactions by performing an attack that spans across multiple functions.
The key factor is the sequence of function calls. If the contract doesn’t properly update its state before performing an external action (e.g., transferring funds, calling an external contract), an attacker can exploit the order of operations and the dependencies between functions to reenter vulnerable points of the contract before it finishes executing its intended logic.
To better illustrate the described cross-function reentrancy attack, let us consider the following code example:
Attack Scenario:
1. Initial Setup: The attacker stakes tokens using the
stake()
function and accumulates rewards through the contract.2. Exploit Flow:
- The attacker calls the
withdraw()
function to withdraw their staked tokens.- Inside the
withdraw()
function, a call is made to transfer the requested amount of tokens back to the attacker.- While the transfer is still in progress, the attacker’s contract has a fallback function that gets triggered whenever Ether is sent to it.
3. Reentering the Contract:
- The fallback function in the attacker's contract reenters the
claimRewards()
function before thewithdraw()
function has finished updating the attacker’s balance.- The attacker can claim rewards based on the old (higher) staked balance, which was not yet updated in the contract state because the
withdraw()
function hasn't completed its execution.
4. Repeat:
- By reentering, the attacker can keep claiming rewards or withdrawing tokens without updating the contract's state properly, leading to an exploitation of the contract’s funds.
This type of reentrancy is particularly dangerous because it can bypass basic protections against reentrancy attacks. While a contract may appear safe at first glance with each individual function seeming to handle its own logic correctly, the combined effect of multiple functions operating on shared state can lead to unexpected behaviors.
B. Cross-Contract Reentrancy Attack
Cross contract reentrancy is another variation of the classic reentrancy attack that spans across multiple contracts. In this variation, instead of exploiting reentrant behavior within a single contract, the attacker exploits the interactions between two or more contracts to manipulate state or logic in unintended ways. This attack leverages the complexity of inter-contract dependencies, often making it harder to detect and mitigate than single-contract reentrancy.
How it works
Smart contracts frequently interact with other contracts to perform tasks such as token transfers, price calculations, staking, lending, or liquidity provisioning. This reentrancy attack is different from the classic reentrancy one, the attacker uses a callback function in the malicious contract to reenter the vulnerable contract through another function or interaction. This creates a more complex attack chain.
Key Differences
C. Read-Only Reentrancy
Read-only reentrancy is another variation of the reentrancy attack, often undervalued because it seems riskless at first.
Unlike traditional reentrancy, where the attacker directly manipulates state variables to steal funds or corrupt the contract, read-only reentrancy targets contracts during read operations (e.g., view or pure functions) to produce misleading or inconsistent results. While these functions do not modify state directly, their return values might depend on external calls, creating an opportunity for reentrancy if an attacker can influence the logic of these calls.
The impact of read-only reentrancy is typically less direct than classic reentrancy attacks but can be severe in specific contexts. For example, it could be used to manipulate oracle prices, mislead external systems, or disrupt decision-making processes within smart contracts that rely on the integrity of read operations.
How it works
1. Dependency on External Calls:
- A vulnerable contract performs a
view
function call to another contract or an external system to fetch data (e.g., token balances, oracle prices).
2. Reentry During External Call:
- The external contract is controlled by an attacker or interacts with a malicious contract. During the call, the attacker triggers a callback into the original contract.
3. Inconsistent State During Reentry:
- The reentrant call causes the original contract to return a manipulated or incorrect value before its logic has completed, leading to unexpected or exploitable behavior.
4. Outcome:
- While state is not directly corrupted, the contract’s decision-making based on the read-only operation is undermined, enabling indirect exploitation.
In other words, the function that enables the attack is one that executes an external call without first updating the contract state. This presents a direct risk because the function may, for instance, implement a nonReentrant modifier designed to prevent reentrancy to the same function. However, since the execution flow is now controlled by the malicious contract through its fallback function, it can exploit the fact that the state of the first contract has not been updated.
Attack Scenario
Imagine a contract that calculates the price of a token using an oracle during a read operation. The contract calls the oracle's getPrice()
function, which depends on external data. An attacker uses read-only reentrancy to disrupt the calculation.
Vulnerable Contract: Ether Pool
This contract allows users to deposit Ether into a shared pool, tracks each user's contributions, and provides a function to check the contract's total balance.
It can be observed that the withdraw()
function executes an external call without first updated the balances and totalBalances state. However, classic reentrancy is not possible within the deposit()
and withdraw()
functions as they implement a nonReentrant modifier. However, getTotalBalance
is vulnerable because it returns the totalBalance without being updated.
External Contract
1. Setup:
- An attacker deploys a malicious contract (see below) and interacts with the vulnerable EtherPool.
2. Triggering the Attack:
- The attacker first deposits Ether into the vulnerable EtherPool.
- The attacker then calls
withdraw()
on EtherPool. During this operation, EtherPool sends Ether to the attacker's contract, triggering its fallback() function.
3. Reentering the Vulnerable Contract:
- Inside the
fallback()
function of the malicious contract, the attacker calls thedistributeRewards()
function on the ExternalContract.-
distributeRewards()
relies on the outdatedgetTotalBalance()
value, which hasn’t been updated yet in EtherPool (because the state updates in withdraw() happen after the external call).
4. Exploit:
- The attacker manipulates the logic of ExternalContract by causing it to act on stale or incorrect data. For example:
- The
distributeRewards()
function might proceed with reward distribution based on a higher-than-actual balance.- ExternalContract might use the outdated total balance for any operation, leading to unintended outcomes.
4. Conclusion
We can conclude that the read-only reentrancy attack is a particularly tricky scenario. It may seem harmless at first, especially if the protocol does not currently have any external integrations. However, if the vulnerability is not addressed, it can evolve into a situation where new parts of the protocol are built on top of the read-only vulnerable contract, as described earlier, making it exploitable. This also poses significant risks to third-party protocols that integrate with the vulnerable one. Therefore, it is always recommended to follow the CEI (Checks-Effects-Interactions) pattern: first performing the necessary checks, then updating the contract state and variables, and finally executing external calls.