The history of Ethereum security is divided into two eras: before The DAO hack in 2016 and after. Back then, a reentrancy attack led to a hard fork and the birth of Ethereum Classic.
Years later, developers relaxed. It is widely believed that the nonReentrant modifier and the Checks-Effects-Interactions pattern have completely solved the issue. This is a mistake. Reentrancy has evolved. Today, we see attacks through token standards (ERC-777) and data reading manipulations (Read-only Reentrancy) that bypass standard protections.
1. The Classic: How It Works The mechanics of a basic attack are simple. A smart contract sends ETH to a user first, and only then updates their balance.
An attacker creates a malicious contract. Upon receiving funds, the fallback or receive function triggers, calling the victim's withdrawal function again. Since the balance hasn't been updated yet (funds haven't been deducted), the victim contract sends money again. The cycle repeats until the treasury is drained.
Solution: Checks-Effects-Interactions pattern (change state first, then send money) and the nonReentrant modifier.
2. Token Attacks: The ERC-777 Threat Many believe reentrancy is only possible with native ETH. However, ERC-777 and ERC-1155 token standards contain hooks—functions triggered upon token receipt.
The Scenario: You write a token deposit function for your protocol using transferFrom. In the ERC-777 standard, transferFrom calls tokensReceived on the recipient and sender addresses. If an attacker sends tokens to your contract, control is handed over to them before the transaction completes. They can call the deposit or withdraw function again, disrupting the share accounting logic.
Conclusion: Even if you use ERC-20, always check if the token acts as a proxy for ERC-777. Use nonReentrant on all state-changing functions.
3. Read-only Reentrancy: The Invisible Enemy This is the most modern and dangerous vector, costing protocols millions of dollars (Curve, Balancer cases). The problem arises when nonReentrant protects write functions but leaves view functions open.
Attack Mechanics:
How to Defend Standard nonReentrant is useless here as it doesn't block view functions. Solution:
Reentrancy represents an architectural vulnerability rather than a simple code bug. It exploits the very nature of external calls in Ethereum. If your protocol interacts with other contracts or uses complex token standards, relying solely on nonReentrant is insufficient. Security requires full control over the transaction execution flow.
Years later, developers relaxed. It is widely believed that the nonReentrant modifier and the Checks-Effects-Interactions pattern have completely solved the issue. This is a mistake. Reentrancy has evolved. Today, we see attacks through token standards (ERC-777) and data reading manipulations (Read-only Reentrancy) that bypass standard protections.
1. The Classic: How It Works The mechanics of a basic attack are simple. A smart contract sends ETH to a user first, and only then updates their balance.
An attacker creates a malicious contract. Upon receiving funds, the fallback or receive function triggers, calling the victim's withdrawal function again. Since the balance hasn't been updated yet (funds haven't been deducted), the victim contract sends money again. The cycle repeats until the treasury is drained.
Solution: Checks-Effects-Interactions pattern (change state first, then send money) and the nonReentrant modifier.
2. Token Attacks: The ERC-777 Threat Many believe reentrancy is only possible with native ETH. However, ERC-777 and ERC-1155 token standards contain hooks—functions triggered upon token receipt.
The Scenario: You write a token deposit function for your protocol using transferFrom. In the ERC-777 standard, transferFrom calls tokensReceived on the recipient and sender addresses. If an attacker sends tokens to your contract, control is handed over to them before the transaction completes. They can call the deposit or withdraw function again, disrupting the share accounting logic.
Conclusion: Even if you use ERC-20, always check if the token acts as a proxy for ERC-777. Use nonReentrant on all state-changing functions.
3. Read-only Reentrancy: The Invisible Enemy This is the most modern and dangerous vector, costing protocols millions of dollars (Curve, Balancer cases). The problem arises when nonReentrant protects write functions but leaves view functions open.
Attack Mechanics:
- An attacker enters a liquidity pool and initiates a swap or liquidity removal.
- During the swap, token balances in the pool change, but the transaction is not yet finalized.
- The attacker uses fallback to seize control. At this moment, they do not re-enter the same contract (protected by the guard).
- Instead, they go to another protocol (e.g., a lending platform) that uses the vulnerable pool as a price oracle.
- The lending protocol calls getVirtualPrice on the pool. Since the pool is in an intermediate state (money left, but recalculation isn't finished), it returns an incorrect, manipulated price.
- The attacker takes out a massive loan against the "cheap" asset or liquidates others' positions at an artificial price.
How to Defend Standard nonReentrant is useless here as it doesn't block view functions. Solution:
- Use the "Reentrancy Guard" pattern for view functions as well (though this increases gas costs).
- Implement entry checks within the architecture itself: if a contract is in the process of state change, it must forbid reading critical data (price, reserves).
Reentrancy represents an architectural vulnerability rather than a simple code bug. It exploits the very nature of external calls in Ethereum. If your protocol interacts with other contracts or uses complex token standards, relying solely on nonReentrant is insufficient. Security requires full control over the transaction execution flow.