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

  1. contract EtherStore {
  2. uint256 public withdrawalLimit = 1 ether;
  3. mapping(address => uint256) public lastWithdrawTime;
  4. mapping(address => uint256) public balances;
  5. function depositFunds() external payable {
  6. balances[msg.sender] += msg.value;
  7. }
  8. function withdrawFunds (uint256 _weiToWithdraw) public {
  9. require(balances[msg.sender] >= _weiToWithdraw);
  10. // limit the withdrawal
  11. require(_weiToWithdraw <= withdrawalLimit);
  12. // limit the time allowed to withdraw
  13. require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
  14. require(msg.sender.call.value(_weiToWithdraw)());
  15. balances[msg.sender] -= _weiToWithdraw;
  16. lastWithdrawTime[msg.sender] = now;
  17. }
  18. }

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

  1. import "EtherStore.sol";
  2. contract Attack {
  3. EtherStore public etherStore;
  4. // intialize the etherStore variable with the contract address
  5. constructor(address _etherStoreAddress) {
  6. etherStore = EtherStore(_etherStoreAddress);
  7. }
  8. function attackEtherStore() external payable {
  9. // attack to the nearest ether
  10. require(msg.value >= 1 ether);
  11. // send eth to the depositFunds() function
  12. etherStore.depositFunds.value(1 ether)();
  13. // start the magic
  14. etherStore.withdrawFunds(1 ether);
  15. }
  16. function collectEther() public {
  17. msg.sender.transfer(this.balance);
  18. }
  19. // fallback function - where the magic happens
  20. function () payable {
  21. if (etherStore.balance > 1 ether) {
  22. etherStore.withdrawFunds(1 ether);
  23. }
  24. }
  25. }

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:

  1. Attack.sol, line 15: The depositFunds function of the EtherStore contract will be called with a msg.value of 1 ether (and a lot of gas). The sender (msg.sender) will be the malicious contract (0x0…​123). Thus, balances[0x0..123] = 1 ether.

  2. Attack.sol, line 17: The malicious contract will then call the withdrawFunds function of the EtherStore contract with a parameter of 1 ether. This will pass all the requirements (lines 12–16 of the EtherStore contract) as no previous withdrawals have been made.

  3. EtherStore.sol, line 17: The contract will send 1 ether back to the malicious contract.

  4. Attack.sol, line 25: The payment to the malicious contract will then execute the fallback function.

  5. Attack.sol, line 26: The total balance of the EtherStore contract was 10 ether and is now 9 ether, so this if statement passes.

  6. Attack.sol, line 27: The fallback function calls the EtherStore withdrawFunds function again and ‘reenters‘ the EtherStore contract.

  7. EtherStore.sol, 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 lastWithdrawTime variable. Again, we pass all the requirements.

  8. EtherStore.sol, line 17: The attacking contract withdraws another 1 ether.

  9. Steps 4–8 repeat until it is no longer the case that EtherStore.balance > 1, as dictated by line 26 in Attack.sol.

  10. Attack.sol, line 26: Once there is 1 (or less) ether left in the EtherStore contract, this if statement will fail. This will then allow lines 18 and 19 of the EtherStore contract to be executed (for each call to the withdrawFunds function).

  11. EtherStore.sol, lines 18 and 19: The balances and lastWithdrawTime 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:

  1. contract EtherStore {
  2. // initialize the mutex
  3. bool reEntrancyMutex = false;
  4. uint256 public withdrawalLimit = 1 ether;
  5. mapping(address => uint256) public lastWithdrawTime;
  6. mapping(address => uint256) public balances;
  7. function depositFunds() external payable {
  8. balances[msg.sender] += msg.value;
  9. }
  10. function withdrawFunds (uint256 _weiToWithdraw) public {
  11. require(!reEntrancyMutex);
  12. require(balances[msg.sender] >= _weiToWithdraw);
  13. // limit the withdrawal
  14. require(_weiToWithdraw <= withdrawalLimit);
  15. // limit the time allowed to withdraw
  16. require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
  17. balances[msg.sender] -= _weiToWithdraw;
  18. lastWithdrawTime[msg.sender] = now;
  19. // set the reEntrancy mutex before the external call
  20. reEntrancyMutex = true;
  21. msg.sender.transfer(_weiToWithdraw);
  22. // release the mutex after the external call
  23. reEntrancyMutex = false;
  24. }
  25. }

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].