Back to blogs
Written by
Ciara Nightingale
Published on
September 2, 2024

The Full Guide on Reentrancy Attacks in Solidity Smart Contracts

What is a Reentrancy Attack in Solidity smart contracts? Learn how blockchain reentrancy attacks work and how to protect your smart contracts from them.

Table of Contents

A Reentrancy Attack, is a vulnerability in smart contracts where a function can be 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.

What is a Solidity Reentrancy Attack?

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:

  1. Checks: Check the state of the caller e.g. require that the caller has a balance to withdraw.
  2. Effects: Update global state e.g. decrement the caller’s balance in a mapping.
  3. Interactions: If the check(s) pass, perform an external call e.g. to transfer tokens.

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.

Image explaining how solidity smart contract reentrancy attacks works
Reentrancy attack leading to drained contract funds

Types of smart contract Reentrancy Attack

1. Single function Reentrancy

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.

2. Cross-function Reentrancy

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.

3. Cross-contract Reentrancy (also known as read-only)

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.

4. Cross-Chain Reentrancy

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.

5. Read-Only Reentrancy

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

Mitigating against Solidity Reentrancy Attacks

1. Use the checks-effects-interactions pattern

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:

  1. Checks: Check the state of the caller e.g. require that the caller has a balance to withdraw.
  2. Effects: Update global state e.g. decrement the caller’s balance in a mapping.
  3. Interactions: If the check(s) pass, perform an external call e.g. to transfer tokens.

Let’s see what this looks like in practice:


mapping (address => uint) public balance;

function withdraw(uint amount) public {
	 // 1. Checks
   require(balance[msg.sender] >= amount);
   // 2. Effects
   balance[msg.sender] -= amount;
   // 3. Interactions
   msg.sender.call{value: amount}("");
   // Note: always emit an event after a change of state
   emit Withdrawal(msg.sender, amount);
}
image showing the check-effect-interactions pattern to mitigate against solidity smart contract reentrancy attacks
Using the Checks-Effects-Interactions pattern to prevent reentrancy attacks

2. Implement mutexes or locks

  • A mutex (mutual exclusion) mechanism prevents a function from being executed multiple times within the same transaction. This is usually achieved using a boolean flag to indicate whether the function is currently executing e.g. isWithdrawing = true.
  • Locks can be used to “lock” the function ensuring that another function call cannot reenter it until the current execution is completed. This prevents the function from being called recursively or reentered from external contracts while its state is still being modified, thus mitigating the risk of reentrancy attacks.
  • One example of a lock is a reentrancy guard: a function modifier (a reusable piece of code that executes before or after a function call), the most commonly used guard is Open Zeppelin’s ReentrancyGuard. This modifier ensures the attacker can’t make more than one function run at a time.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.18;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract ReentracyProtected is ReentrancyGuard {
	mapping(address => uint) public balances; 
	
	function withdraw() external nonReentrant {
        uint balance = balances[msg.sender];
        require(balance > 0, "Insufficient balance");
        balances[msg.sender] = 0;
        (bool success, ) = address(msg.sender).call{ value: balance }("");
        require(success, "Failed to withdraw");
    }
}

3. Perform extensive code review and testing:

  • Audits: Multiple rounds of smart contract audits including private and competitive audits are recommended and can significantly decrease the chance of a reentrancy attack occurring.
  • Thorough Testing: Audits are not a fail-safe protection against exploits. To ensure smart contract security, thorough testing, including smart contracts invariants testing, is required.

Examples of Smart contract Reentrancy Attack

  • The DAO Hack: One of the most infamous examples of a reentrancy attack occurred in 2016 with the exploitation of The DAO, a decentralized investment fund built on Ethereum. A vulnerability in the DAO's smart contract allowed an attacker to repeatedly withdraw funds before the contract could update its balance, resulting in the theft of ~$6 million worth of Ether. The incident prompted a contentious hard fork of the Ethereum blockchain to reverse the unauthorized transactions and restore the stolen funds.
  • Curve Finance: On July 30th, 2023, the decentralized finance (DeFi) protocol Curve Finance fell victim to a reentrancy attack, as a result of a Vyper compiler bug, resulting in the theft of almost $70 million.

Reentrancy audit example

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.


/// @dev Split the units of `_tokenID` owned by `account` across `_values`
    /// @dev `_values` must sum to total `units` held at `_tokenID`
    function _splitValue(address _account, uint256 _tokenID, uint256[] calldata _values) internal {
        // ... //
        uint256 valueLeft = tokenValues[_tokenID];
        // ... //

        for (uint256 i; i < len;){
        		valueLeft -= values[i];

            tokenValues[toIDs[i]] = values[i];

            unchecked {
                ++i;
            }
        }
        //
        // @audit CRITICAL Re-entrancy attack due to not following the Check-Effects-Interaction pattern
        //
        // ERC1155._mintBatch() will call _account.onERC1155BatchReceived() if
        // _account is contract. AttackContract.onERC1155BatchReceived() can hijack execution
        // flow by re-entering _splitValue() via HypercertMinter.splitValue() many times to mint a huge amount
        // of fractions for the same _tokenID as the decreased valueLeft has not been written to storage before
        // calling ERC1155._mintBatch()

        _mintBatch(_account, toIDs, amounts, "");

        tokenValues[_tokenID] = valueLeft;

        emit BatchValueTransfer(typeIDs, fromIDs, toIDs, values);
    }

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.

Conclusion what is a Solidity reentrancy attack

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.

Secure your protocol today

Join some of the biggest protocols and companies in creating a better internet. Our security researchers will help you throughout the whole process.
Stay on the bleeding edge of security
Carefully crafted, short smart contract security tips and news freshly delivered every week.