Reentrancy Unleashed

When I first started diving into the world of Web3 during a development course, I stumbled upon the concept of reentrancy. It was one of those moments where curiosity took over — I couldn’t believe how a small oversight in a smart contract’s code could lead to such catastrophic outcomes. This vulnerability quickly became my gateway into exploring the broader realm of blockchain security. Reentrancy attacks taught me just how crucial it is to build secure smart contracts, especially when dealing with users’ funds. Today, I’m excited to share insights on the four types of reentrancy vulnerabilities and how we can safeguard our code against them.

Classic Reentrancy

Classic reentrancy is the most well-known form of this vulnerability. It occurs when an external contract repeatedly calls a function before the state of the contract is updated. This allows the attacker to exploit the system and perform unintended actions, like draining funds before the contract can record the withdrawal.

Example

Consider a contract that allows users to deposit and withdraw Ether. The vulnerability arises when the contract makes an external call (e.g., sending Ether) before updating the user’s balance.

In this example, the contract is vulnerable because it sends funds before updating the balance to zero, enabling an attacker to exploit this order of operations.

Solution

The solution involves updating the contract’s state before making the external call, thereby preventing reentrant behavior:

By updating the balance before the external call and using the nonReentrant modifier from OpenZeppelin's ReentrancyGuard, this approach ensures that no reentrancy attack can occur.

The DAO Hack of 2016: Ethereum’s First Major Crisis

In 2016, a reentrancy vulnerability in the DAO’s smart contract led to one of the biggest hacks in blockchain history, with $60 million in Ether stolen. This event forced the Ethereum community to choose between letting the hack stand or reverting the blockchain. The decision to hard fork split the network into two: Ethereum (ETH) and Ethereum Classic (ETC), with the latter preserving the original chain, including the hack’s impact.

Cross-Function Reentrancy

Cross-function reentrancy happens when an attacker uses multiple functions that share state variables to exploit reentrancy. By calling a vulnerable function that interacts with shared state through another method, an attacker can manipulate the state of the contract.

Example

In this example, a contract has separate functions for withdrawal and fund transfer:

Here, the _transfer function is called during a withdrawal but the state is updated afterward, making it possible for an attacker to exploit this sequence.

Solution

To prevent this, update the state before making any external calls and use the nonReentrant modifier:

Cross-Contract Reentrancy

In a cross-contract reentrancy attack, the attacker exploits the interaction between multiple contracts. The vulnerable contract (Contract A) makes an external call to another contract (Contract B, controlled by the attacker), allowing the attacker to re-enter Contract A through different points of interaction before the state in Contract A is properly updated. While the attacker may re-enter the same function (such as withdraw), the vulnerability here is rooted in how two or more contracts interact with each other.

Example of a Vulnerable Contract (Contract A):

Example of the Attacker’s Contract (Contract B):

How the Cross-Contract Reentrancy Attack Works

  1. Setup: The attacker deploys two contracts: the attacker’s contract (Contract B) and interacts with the vulnerable contract (Contract A).

  2. Deposit Funds: The attacker deposits 1 Ether into the vulnerable contract (Contract A) using the deposit() function.

  3. Trigger the Attack: The attacker then calls callAnotherContract() in Contract A, which triggers a function (someFunction()) in the attacker’s contract (Contract B).

  4. Re-enter withdraw(): Inside someFunction() in Contract B, the attacker re-enters Contract A’s withdraw() function. Because the state (i.e., the balance) in Contract A hasn’t been updated yet, the attacker can withdraw the funds multiple times.

  5. Drain Funds: The attacker repeats this process, draining funds from Contract A, using Contract B as the entry point for reentrancy.

Solution

To prevent this type of attack, always update the contract state before making any external calls, and use the nonReentrant modifier to block re-entrance into functions. Here’s a secured version of Contract A:

Read-Only Reentrancy

This is a newer kind of reentrancy. Unlike other types, read-only reentrancy doesn’t directly change the contract state but involves making calls to view or pure functions that provide exploitable data. Attackers use these calls to manipulate decisions within the contract.

Example

A contract might have a function that checks a user’s balance before allowing a withdrawal:

Solution

Avoid using view functions for critical checks and always update the state before external calls:

Bonus: ERC777 Token Reentrancy Vulnerability

While reentrancy attacks are commonly associated with Ether transfers, they can also affect token standards, especially in the case of ERC777 tokens. I first came across this issue while exploring security vulnerabilities in tokens and found a reference to it in the Weird ERC20 Tokens GitHub repository, which catalogs various strange behaviors and pitfalls in token contracts.

What Is ERC777?

ERC777 is an advanced Ethereum token standard that was designed to improve upon the widely used ERC20 standard. It introduced several new features, such as offering hooks that allow contracts and wallets to react when tokens are received, making it more versatile for certain use cases.

The Reentrancy Issue

However, one downside of these hooks is that they open up the potential for reentrancy attacks. When an ERC777 token is transferred, it triggers a tokensReceived hook, which can be exploited if the receiving contract re-enters into a vulnerable function. Unlike Ether transfers, which require manual checks for reentrancy, ERC777’s automatic hooks make it easier for an attacker to exploit reentrancy vulnerabilities if the contract does not handle the hooks carefully.

Example

Here’s a simplified illustration of how this can happen:

Solution

To mitigate this, the usual best practices apply: ensure that the contract’s state is updated before making external calls, and avoid trusting the behavior of external contracts during sensitive operations.

Further Reading

For anyone curious about other unusual behaviors in token contracts, I highly recommend checking out the Weird ERC20 Tokens GitHub repository. It’s a great resource for learning about obscure or unexpected vulnerabilities, including the reentrancy issue in ERC777. Understanding these vulnerabilities is crucial as we continue to improve smart contract security in the DeFi ecosystem.

Wrapping It Up

Reentrancy attacks are like exploiting a glitch in an ATM machine. Imagine if you could withdraw money multiple times before the ATM updates its records to reflect that your account is empty. In smart contracts, reentrancy works in a similar way — attackers repeatedly call a vulnerable function (like withdraw) before the contract updates its state, allowing them to drain funds.

The common theme across all types of reentrancy vulnerabilities — classiccross-functioncross-contract, and read-only — is that the contract must update its state before making any external calls. This is critical because when a contract interacts with another contract or sends Ether, there’s a window of opportunity for malicious contracts to re-enter the function and exploit the time gap before the state (such as the user’s balance) is updated.

Subscribe to z0L
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.