• YouTube Channel
  • System Status
  • VS Code Extension
  • Potential Reentrancy

    Overview

    What is the Reentrancy vulnerability?

    In Ethereum, program logic encoded in smart contracts is capable of calling logic in separate contracts, which may be authored by separate entities. This aspect of the system's design leaves open the possibility that chains of function calls may ultimately invoke a function that was already called earlier in the chain, "re-entering" its body. Depending on what statements in the body of the re-entered function follow the departure point where it originally invoked an external contract, the execution logic may differ from what the developer intended, and may lead to serious issues with overall smart contract execution.

    More concretely, functions in Ethereum that make external calls (which could ultimately re-enter that same function), and subsequently modify some state in the Ethereum Virtual Machine (EVM), may lead to executions where state is inconsistent, because the original function never completed its state modifications before being re-entered. Note that it is possible that these state modifications may be "remote" from the original function rather than inline statements, as long as they are reachable after the external call.

    Further reading: Smart Contract Security Field Guide: Reentrancy

    Technical example of vulnerable code

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract VulnerableEtherWallet {
          // Mapping from addresses to their balances
          mapping(address => uint256) public balances;
    
          /**
           * Allows anyone to deposit Ether into their wallet balance.
           */
          function deposit() external payable {
              require(msg.value > 0, "Deposit amount must be greater than 0");
              balances[msg.sender] += msg.value;
          }
    
          /**
           * Allows users to withdraw Ether from their wallet balance.
           * This function contains a reentrancy vulnerability.
           */
          function withdraw(uint256 _amount) public {
              require(balances[msg.sender] >= _amount, "Insufficient balance");
    
              // Vulnerability: Calling an external address before updating the sender's balance.
              (bool sent, ) = msg.sender.call{value: _amount}("");
              require(sent, "Failed to send Ether");
    
              // The sender's balance is updated after sending Ether.
              // An attacker can re-enter this function before this line executes if they control the called contract.
              balances[msg.sender] -= _amount;
          }
    
          /**
           * Returns the balance of the caller.
           */
          function getBalance() public view returns (uint256) {
              return balances[msg.sender];
          }
      }
    
    

    In the above example of a simple Ether wallet, the withdraw() function makes an external call to transfer a requested amount of Ether to msg.sender, but performs this call before actually updating the caller's balance in the balances array. Thus, if the caller is able to reenter this function, they could withdraw more than their allotted balance, potentially draining the entire contract of its funds.

    Technical example of how to fix the vulnerability

      // SPDX-License-Identifier: Unlicense
      pragma solidity ^0.8.0;
    
      contract CorrectedEtherWallet {
          // Mapping from addresses to their balances
          mapping(address => uint256) public balances;
    
          /**
           * Allows anyone to deposit Ether into their wallet balance.
           */
          function deposit() external payable {
              require(msg.value > 0, "Deposit amount must be greater than 0");
              balances[msg.sender] += msg.value;
          }
    
          /**
           * Allows users to withdraw Ether from their wallet balance.
           */
          function withdraw(uint256 _amount) public {
              require(balances[msg.sender] >= _amount, "Insufficient balance");
    
              balances[msg.sender] -= _amount;
    
              (bool sent, ) = msg.sender.call{value: _amount}("");
              require(sent, "Failed to send Ether");
          }
    
          /**
           * Returns the balance of the caller.
           */
          function getBalance() public view returns (uint256) {
              return balances[msg.sender];
          }
      }
    
    

    In the corrected example above, the withdraw() function updates the caller's balance before transferring Ether to them. Even if the caller reenters this function, they will not be able to withdraw more than their allotted balance of Ether, as it will have already been decreased before the first external call was made.