The Full Guide on Reentrancy Attacks in Solidity Smart Contracts

This comprehensive guide on reentrancy attacks in Solidity smart contracts explains the mechanics, types, and mitigation strategies, including the checks-effects-interactions pattern, mutexes, and extensive code review, to protect against reentrancy vulnerabilities in decentralized applications (dApps).

June 5, 2024

The Full Guide on Reentrancy Attacks in Solidity Smart Contracts

Introduction

Reentrancy attacks pose one of the most critical security challenges in Solidity smart contracts, often resulting in severe financial losses and compromising the integrity of decentralized applications (dApps). Understanding and mitigating these attacks is essential for Web3 security researchers, audit firms, and smart contract developers. This comprehensive guide delves into the mechanics of reentrancy attacks, their types, mitigation strategies, and real-world examples, providing a thorough understanding of how to protect your smart contracts from these vulnerabilities.

What is a Solidity Reentrancy Attack?

A reentrancy attack in Solidity smart contracts occurs when an external contract repeatedly calls a function before its initial execution is complete, exploiting the contract’s state inconsistencies. This typically happens via an external call (e.g., a fallback function or onERC721Received), allowing the attacker to manipulate the contract's state and drain its funds.

For example, consider a function that follows these steps:

  1. Checks: Verify the caller's balance.
  2. Effects: Update the caller's balance.
  3. Interactions: Transfer tokens to the caller.

If the state update happens after the external call, an attacker can reenter the function with the state unchanged, repeatedly draining the contract.

Types of Smart Contract Reentrancy Attacks

1. Single Function Reentrancy

This basic form occurs when a single function is reentered. The function modifies the contract's state and then calls an external contract without first updating its internal state variables.

2. Cross-Function Reentrancy

Cross-function reentrancy happens when one function performs an external call before updating the state, and the external contract calls another function dependent on this state, leading to unintended interactions.

3. Cross-Contract Reentrancy

This type involves interactions between multiple contracts sharing state. If the state in the first contract is not updated before an external call, other contracts depending on the shared state can be reentered.

4. Cross-Chain Reentrancy

Involving interactions between contracts on different blockchains, this scenario arises in interoperability protocols or decentralized exchanges (DEXs), adding complexity due to interactions across distinct blockchain ecosystems.

5. Read-Only Reentrancy

Known as "read-only external call reentrancy," this vulnerability occurs when an external call is made to another contract that reads data and reenters the calling contract, potentially causing unexpected behavior.

Mitigating Against Solidity Reentrancy Attacks

1. Use the Checks-Effects-Interactions Pattern

Ensure state changes are made before interacting with external contracts or sending Ether. Here’s how it looks 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}("");
  emit Withdrawal(msg.sender, amount);
}


2. Implement Mutexes or Locks

A mutex (mutual exclusion) mechanism prevents a function from being executed multiple times within the same transaction. This is often achieved using a boolean flag or a reentrancy guard from libraries like OpenZeppelin:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract ReentrancyProtected 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

Conduct multiple rounds of smart contract audits, including private and competitive audits, and thorough testing to ensure smart contract security.

Examples of Smart Contract Reentrancy Attacks

The DAO Hack

In 2016, the DAO, a decentralized investment fund, was exploited through a reentrancy attack, resulting in the theft of ~$6 million worth of Ether. The vulnerability allowed an attacker to repeatedly withdraw funds before the contract could update its balance, prompting a contentious hard fork of the Ethereum blockchain.

Curve Finance

On July 30th, 2023, Curve Finance fell victim to a reentrancy attack due to a Vyper compiler bug, leading to the theft of almost $70 million.

HypercertMinter::splitValue Vulnerability

A vulnerability in the HypercertMinter contract allowed tokens to be split into fractions without adhering to the checks-effects-interactions pattern, enabling reentrancy:

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;
       }
   }
   _mintBatch(_account, toIDs, amounts, "");
   tokenValues[_tokenID] = valueLeft;
   emit BatchValueTransfer(typeIDs, fromIDs, toIDs, values);
}


This code was vulnerable because _mintBatch called an external function before updating the internal state.

Conclusion

Reentrancy attacks in Solidity smart contracts are a serious security threat, but with proper understanding and mitigation techniques, they can be effectively prevented. Key strategies include using the checks-effects-interactions pattern, implementing mutexes, and conducting thorough audits and testing. Real-world examples like the DAO hack and Curve Finance incident highlight the importance of these measures.

References

Author's image

TRUSTBYTES