What Is a Reentrancy Attack in Solidity Smart Contract? With Examples

Introduction

Reentrancy Attack is one of the most common attacks in Ethereum and other blockchain platforms. In this article we will look at how reentrancy attack works and how to prevent it.

 

What Is Reentrancy Attack?

A reentrancy attack is a malicious exploit carried out on a program that permits multiple levels of recursion. In general, the attacker subverts a program's state by creating a malicious recursion loop. The concept of reentrancy attacks is not limited to Ethereum or smart contracts. Reentrancy attacks are possible in any computing platform which supports recursion, including JavaScript and web application languages.

 

How to Detect Reentrancy Attack?

In Ethereum smart contracts, reentrancy is a serious attack vector. It is a security vulnerability caused by the fact that multiple functions can be executed in the same transaction. This makes it possible for an attacker to call multiple functions of a smart contract and make changes to its state, even after those functions have completed.

As with most things when it comes to vulnerabilities, the best way to detect them is still a manual, detailed review of the source code which we do as part of smart contract audit service. In the example below we have a code vulnerable to reentrancy attack.

 

pragma solidity 0.8.13;

contract InsecureEtherVault {
    mapping (address => uint256) private userBalances;

    function deposit() external payable {
        userBalances[msg.sender] += msg.value;
    }

    function withdrawAll() external {
        uint256 balance = getUserBalance(msg.sender);
        require(balance > 0, "Insufficient balance");

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

        userBalances[msg.sender] = 0;
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) public view returns (uint256) {
        return userBalances[_user];
    }
}  


The withdrawAll function pays the user his balance, which is saved and defined in the smart contract. The approach used to execute the payment is: (bool success, ) = msg.sender.call{value: balance}("");

In solidity, msg.sender is the address from which the smart contract was called. That address can be an Ethereum wallet or an attacker's smart contract address if the withdrawAll function in the vulnerable code is called from it.

The problem lies in the fact that the vulnerable code blindly calls the call function whose content may be malicious. In order for the attacker to take advantage of this vulnerability, he will add a call to the same function again to the content of the call function of the contract from which the vulnerable code will be called.

 

How to Prevent Reentrancy Attack?

Protection against this type of attack is actually quite simple and reminds a little of the race condition vulnerabilities that we find in classic (web) applications these days. The high level idea is to add a check at the beginning of the function execution to see if the function is already being executed, then add the property locked = true and send the tokens. After that, let's return the locked property to false so that the function is free again.

function withdraw() external {
​ ​ ​​require(!locked);
​ ​ ​​locked = true;​ ​ 
​​  uint userBalance = userBalances[msg.sender];
 ​ ​​require(userBalance > 0);
​ ​ ​​(bool success,) = msg.sender.call{ ​​value: ​userBalance ​​}("");
​ ​ ​​require(success,);
​ ​ ​​userBalances[msg.sender] = 0;​ ​ ​​
  locked = false;
}