Detecting Reentrancy Issues in Smart Contracts Using Fuzzing

Photo courtesy of Asael Peña

In previous posts, we introduced Harvey, a fuzzer for Ethereum smart contracts, and presented two techniques to boost its effectiveness: input prediction and multi-transaction fuzzing.

Harvey is being developed by ConsenSys Diligence in collaboration with Maria Christakis from MPI-SWS. It is one of the tools that powers the MythX platform. Sign up for our beta to give it a try!

Starting with the DAO attack, reentrancy issues have been exploited several times over the last few years to hack smart contracts. In previous posts, we saw examples of how we can use Harvey to detect assertion violations in smart contracts.

In this post, we will explain how Harvey is able to detect other issues and in particular reentrancy issues.

Motivating Example

Let’s look at the following smart contract (written in the Solidity programming language) to illustrate two reentrancy issues.

pragma solidity ^0.4.25;

contract BuggyBank {
  mapping (address => uint256) private balance;

  function Deposit() public payable {
    balance[msg.sender] += msg.value;
  }

  function WithdrawVeryBuggy(uint256 amount) public {
    uint256 bal = balance[msg.sender];
    require(amount <= bal);
    require(msg.sender.call.value(amount)());
    balance[msg.sender] -= amount;
  }

  event BalanceDecreased(address addr);

  function WithdrawBuggy(uint256 amount) public {
    uint256 bal = balance[msg.sender];
    require(amount <= bal);
    balance[msg.sender] -= amount;
    require(msg.sender.call.value(amount)());
    if (balance[msg.sender] != bal) {
      emit BalanceDecreased(msg.sender);
    }
  }
}

The contract implements functionality for depositing (function Deposit) and withdrawing (functions WithdrawBuggy and WithdrawVeryBuggy) crypto assets (ether in our case). Many types of smart contracts, such as wallets and tokens, also implement such functionality. The contract manages balances of users by storing them in a map balance.

When withdrawing the assets in function WithdrawVeryBuggy, the contract calls the sender of the transaction (i.e., the “owner” of the assets) on Line 13 in order to transfer the assets. Such external calls are very common in many contracts, but may lead to subtle issues if a call re-enters the calling contract. In our example, assuming the callee (i.e., owner being called) is another contract, it could have been programmed by an attacker to call WithdrawVeryBuggy again. This is possible since the owner’s balance is only updated after the call and, eventually, could allow the attacker to drain the contract’s assets. A similar scenario was exploited in the attack on the DAO contract.

Preventing state updates after external calls

To detect such issues, Harvey warns the user about contracts that update the persistent state after external calls. It does so by monitoring the execution of every test input that is generated by the fuzzer. Such a runtime monitor records detected issues on-the-fly and one can easily create new detection components using a custom tracer that is hooked into the tracing component of the underlying Ethereum virtual machine (EVM) implementation (go-ethereum in our case).

Harvey emits a warning (including a runnable test input) if it is able to generate a successful transaction that updates the persistent state (storage or balances) after an external call. Note that, in general, it is very difficult to automatically generate a concrete exploit for vulnerable contracts since one would need to synthesize the code of the callee contract (for example, to call back into a specific function with specific arguments under specific circumstances).

To prevent this issue, the developer could move the balance update before the external call.

Preventing state reads after external calls

Function WithdrawBuggy demonstrates this, but contains another subtle flaw. After the external call, the code emits an event if the balance was decreased. At least that seems to be the intention.

However, in our example, this event could also be triggered if the balance was increased since the callee could trigger a call-back by invoking Deposit. The reason for this is that there is a read of the persistent state after the external call. A developer might wrongly assume that the state was not changed during the external call.

Even though this issue may seem less critical, it constitutes a flaw in the contract’s business logic that should be fixed or at the very least reviewed. Imagine a front-end component reacting to such wrongly emitted events (for example, by suggesting donations to the user whose balance supposedly decreased).

Harvey will warn developers about this potential issue if it is able to generate a transaction that performs a read operation on the persistent state of a contract (storage and balance) after an external call. Like for state writes, Harvey is able to detect this issue by directly monitoring the execution of the EVM.

One way to prevent this issue is by reading the state right before the external call. This tends to work well in cases where developers implicitly assume that external calls do not modify the contract’s state. However, in general, a fix should be in line with the developer’s intention. For instance, in our example, the developer might decide that call-backs should be prevented in the first place or that events should only be emitted within the outermost call (and not within call-backs).

Detecting other issues

Harvey also uses runtime monitoring to detect other types of issues in smart contracts (currently SWC-101, SWC-104, SWC-107, SWC-110, SWC-123, SWC-124, and SWC-127), and can be easily extended by writing custom detection monitors. A later post might describe how to easily write such a runtime monitor in Go.

In this post, we have illustrated how reentrancy issues can make smart contracts vulnerable to attacks and how Harvey is able to detect them using fuzzing and runtime monitoring of concrete executions of a contract.

In the next post of this series, we will look at how to use Harvey to analyze systems with several contracts. Stay tuned!

In the meanwhile, sign up for our beta if you want to try Harvey out yourself!

Thanks to Maria Christakis, Joran Honig, John Mardlin (aka Maurelian), Mike Pumphrey, and Gerhard Wagner for feedback on drafts of this article.

More posts chevronRight icon