One of the features of Ethereum smart contracts is their ability to call and utilize code from other external contracts.
Contracts also typically handle ether, and as such often send ether to various external user addresses. These operations require the contracts to submit external calls. These external calls can be hijacked by attackers, who can force the contracts to execute further code (through a fallback function), including calls back into themselves.
Attacks of this kind were used in the infamous DAO hack.
Understanding the Vulnerability
This type of attack can occur when a contract sends ether to an unknown address. An attacker can carefully construct a contract at an external address that contains malicious code in the fallback function.
Thus, when a contract sends ether to this address, it will invoke the malicious code. Typically the malicious code executes a function on the vulnerable contract, performing operations not expected by the developer.
The term “reentrancy” comes from the fact that the external malicious contract calls a function on the vulnerable contract and the path of code execution “reenters” it.
To clarify this, consider the simple vulnerable contract in
EtherStore.sol, which acts as an Ethereum vault that allows depositors to withdraw only 1 ether per week:
This contract has two public functions,
depositFunds function simply increments the sender’s balance.
withdrawFunds function allows the sender to specify the amount of
wei to withdraw.
This function is intended to succeed only if the requested amount to withdraw is less than 1 ether and a withdrawal has not occurred in the last week.
The vulnerability is in line 17, where the contract sends the user their requested amount of ether.
Consider an attacker who has created the contract in
How might the exploit occur?
First, the attacker would create the malicious contract (let’s say at the address
0x0… 123) with the
EtherStore’s contract address as the sole constructor parameter.
This would initialize and point the public variable
etherStore to the contract to be attacked.
The attacker would then call the
attackEtherStore function, with some amount of ether greater than or equal to 1 — let’s assume 1 ether for the time being.
In this example, we will also assume a number of other users have deposited ether into this contract, such that its current balance is 10 ether. The following will then occur:
Attack.sol’s line 15: The
depositFundsfunction of the
EtherStorecontract will be called with a
msg.valueof 1 ether (and a lot of gas). The sender (
msg.sender) will be the malicious contract (
0x0… 123). Thus,
balances[0x0..123] = 1 ether.
Attack.sol’s line 17: The malicious contract will then call the
withdrawFundsfunction of the
EtherStorecontract with a parameter of 1 ether. This will pass all the requirements (lines 12–16 of the
EtherStorecontract) as no previous withdrawals have been made.
EtherStore.sol’s line 17: The contract will send 1 ether back to the malicious contract.
Attack.sol’s line 25: The payment to the malicious contract will then execute the fallback function.
Attack.sol’s line 26: The total balance of the
EtherStorecontract was 10 ether and is now 9 ether, so this if statement passes.
Attack.sol’s line 27: The fallback function calls the
withdrawFundsfunction again and ‘reenters’ the
EtherStore.sol’s line 11: In this second call to
withdrawFunds, the attacking contract’s balance is still 1 ether as line 18 has not yet been executed. Thus, we still have
balances[0x0..123] = 1 ether. This is also the case for the
lastWithdrawTimevariable. Again, we pass all the requirements.
EtherStore.sol’s line 17: The attacking contract withdraws another 1 ether.
- Steps 4–8 repeat until it is no longer the case that
EtherStore.balance > 1, as dictated by line 26 in
Attack.sol’s line 26: Once there is 1 (or less) ether left in the
EtherStorecontract, this if statement will fail. This will then allow lines 18 and 19 of the
EtherStorecontract to be executed (for each call to the
EtherStore.sol, lines 18 and 19: The balances and
lastWithdrawTimemappings will be set and the execution will end.
The final result is that the attacker has withdrawn all but 1 ether from the
EtherStore contract in a single transaction.
There are a number of common techniques that help avoid potential reentrancy vulnerabilities in smart contracts.
The first is to (whenever possible) use the built-in transfer function when sending ether to external contracts. The transfer function only sends 2300 gas with the external call, which is not enough for the destination address/contract to call another contract (i.e., reenter the sending contract).
The second technique is to ensure that all logic that changes state variables happens before ether is sent out of the contract (or any external call). In the
EtherStore example, lines 18 and 19 of
EtherStore.sol should be put before line 17.
It is good practice for any code that performs external calls to unknown addresses to be the last operation in a localized function or piece of code execution. This is known as the checks-effects-interactions pattern.
A third technique is to introduce a mutex — that is, to add a state variable that locks the contract during code execution, preventing reentrant calls.
Applying all of these techniques (using all three is unnecessary, but we do it for demonstrative purposes) to
EtherStore.sol, gives the reentrancy-free contract:
The DAO (Decentralized Autonomous Organization) attack was one of the major hacks that occurred in the early development of Ethereum.
At the time, the contract held over $150 million. Reentrancy played a major role in the attack, which ultimately led to the hard fork that created Ethereum Classic (ETC). For a good analysis of the DAO exploit, check this blog.
More information on Ethereum’s fork history, the DAO hack timeline, and the birth of ETC in a hard fork can be found in (