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.send(...) in Solidity smart contracts. Previously these functions were considered reentrancy-safe, which they aren’t any longer.
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: https://github.com/ChainSecurity/constantinople-reentrancy
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.
The attacker will call the
attack function on his own contract, so that the following events unfold within one transaction:
- The attacker sets the current split using
updateSplitin 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.
- The attacker contract calls the
splitFundsfunction, which will perform the checks*, and send the full deposit of this pair to the contract using a
- From to the fallback function, the attacker updates the split again, this time assigning all funds to his second account.
- The execution of
splitFundscontinues 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.
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 using
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
To test if you are vulnerable:
(a) check if there are any operations following a
(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.
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.
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.