A Reentrancy Attack is the exploitation of a vulnerability in smart contracts where a function is repeatedly called before its previous execution completes, potentially allowing an attacker to manipulate the contract's state.
This article will answer the question: what is a Solidity reentrancy attack? Covering the mechanics of reentrancy attacks, their various types, mitigation strategies, and real-world smart contract examples, in-depth.
So let's get started.
In the context of Solidity smart contracts, a reentrancy attack is when the execution flow is transferred to an external contract, usually via an external call (e.g. a “fallback” function or “onERC721Received’), allowing the function (or another function) to be called recursively. This allows the external contract to re-enter the contract, enabling it to manipulate the state before execution is completed.
Reentrancy is a state synchronization problem occurring when the state is not updated before making an external call. This means that when the function is reentered, the state is the same as the first call.
For example, if the function performed the following actions:
Since the state is updated AFTER performing the external call, if the caller was a contract, and in their fallback function they re-called the same function, they would still pass step (1), enabling them to drain the contract’s funds. This code execution order, as will be described shortly, is NOT the correct pattern to avoid reentrancy attacks.
This can lead to unexpected behavior, allowing an attacker to manipulate the contract's state and potentially drain its funds.
This is the simplest type of reentrancy attack that occurs when a single function within the contract is re-entered. This typically happens when the function modifies the contract's state and then calls an external contract or sends Ether without first updating its internal state variables.
Cross-function reentrancy occurs when one function performs an external call before updating the state and the external contract calls another function that depends on this state. This can lead to unexpected interactions between different parts of the contract, allowing an attacker to exploit vulnerabilities in one function to manipulate the state of another.
Cross-contract reentrancy involves interactions between functions within multiple contracts where the state is shared. As before, if the shared state in the first contract is not updated before an external call, contracts that depend on the shared state can be re-entered.
Cross-chain reentrancy, although less common, involves interactions between smart contracts deployed on different blockchain networks. This scenario can arise in interoperability protocols or decentralized exchanges (DEXs) that facilitate transactions across multiple blockchains. While the mechanics are similar to cross-contract reentrancy, the complexity increases due to interactions between distinct blockchain ecosystems.
For more information on cross-chain reentracy including an example, visit the following blog written by Mateocesaroni.
Also known as "read-only external call reentrancy," this refers to a specific type of reentrancy vulnerability where an external call is made to another contract, but the called contract's function does not modify its state. Instead, the called function reads data from the calling contract and then reenters the calling contract, potentially causing unexpected behavior. Although the called function does not modify state, it can still influence the control flow or behavior of the calling contract, posing a security risk.
For an example of read-only reentrancy visit SunWeb3Sec’s DeFiVulnLabs common smart contract vulnerabilities repository.
For a code example of a reentrancy attack, visit Cyfrin Updraft’s example exploits repository
Ensure that state changes are made before interacting with external contracts or sending Ether. Examining the example described earlier, modifying the steps to follow the Checks-Effects-Interactions pattern:
Let’s see what this looks like in practice:
isWithdrawing = true
.ReentrancyGuard
. This modifier ensures the attacker can’t make more than one function run at a time.
The following code snippet shows a vulnerability found in HypercertMinter::splitValue
which allows a token to be split into fractions.
This function calls SemiFungible1155::_splitValue
which calls _mintBatch()
before writing the decreased value to storage. Therefore, the checks-effects-interactions pattern is not adhered to and thus the function is vulnerable to a reentrancy attack.
_mintBatch()
calls onERC1155BatchReceived()
on _account
if _account
is a contract; therefore the attacker can transfer execution flow to an attacking contract and call HypercertMinter::splitValue
to split the same tokenId
many times to mint a huge amount of fractions.
This exploit is from Pashov's audit of Hypercerts.
For more details on this vulnerability and other examples of reentrancy audit findings, refer to Dacian’s Reentrancy Deep Dive article.
This article explored reentrancy attacks in smart contracts, detailing their mechanics, types, mitigation strategies, and real-world examples like The DAO Hack and Curve Finance incident, emphasizing the importance of security measures and auditing in preventing such attacks.
Getting your protocol audited significantly decreases the probability of an attack like this happening.