What is a reentrancy attack?

A procedure is said to be re-entrant if its execution can be interrupted in the middle, re-initiated, and both runs can complete without any execution errors. In the context of Ethereum smart contracts, re-entry can lead to serious vulnerabilities.

The most famous example of this was the DAO hack, in which $70 million worth of Ether was withdrawn.

So what is a reentry vulnerability? How does it work and how to prevent it?

Mechanism

An example of a reentrant process would be sending an email. The user can start typing an email, save a draft, send another email, and finish the message later. This is a harmless example. However, imagine a poorly designed online banking system for issuing wire transfers, where the account balance is checked only at the initialization stage. A user can initiate multiple transfers without actually sending any of them. The banking system will confirm that there is sufficient balance in the user’s account for each individual transfer. If there was no additional verification at the time of the actual send, the user could then send all transactions and potentially exceed their balance. This is the main re-entry exploit mechanism used in the famous DAO hack.

Real life example – DAO hack

DAO is a popular decentralized investment fund based on smart contracts. In 2016, the DAO smart contract accumulated over $150,000,000 worth of ether (at the time). If the project requesting funding received sufficient support from the DAO community, that project’s Ethereum address could withdraw Ethereum from the DAO. Unfortunately for the DAO, the transfer mechanism was transferring ether to an external address before updating its internal state and noting that the balance had already been transferred. This gave attackers the opportunity to withdraw more Ether than they were entitled to by re-entry.

The DAO hack took advantage of the Ethereum fallback feature to re-entry. Each bytecode of an Ethereum smart contract contains a so-called default fallback function, which has the following implementation.

contract EveryContract {
	function () public {
  }
} 

This default function may contain arbitrary code if the developer overrides the default implementation. In case of redefining a function like payable, the smart contract can receive ether. The function is executed whenever ether is transferred to the contract (send(), transfer() and call() methods).

Beyond the call payable methods, Solidity supports three ways to transfer ether between wallets and smart contracts. Supported ether transfer methods are send(), transfer() and call.value(). Methods differ in how much gas they pass on transmission to execute other methods (in case the recipient is a smart contract) and how they handle exceptions. send() and call().value() will simply return false on failure, but transfer() will throw an exception that will reset everything to its original state before the function was called. These methods are briefly described below.

Ways of broadcasting.  By default, all remaining gas is available using call.value(), but the developer can reduce the amount.
Ways of broadcasting. By default, all remaining gas is available using call.value(), but the developer can reduce the amount.

In the case of the DAO smart contract (the basic version of which is presented below), the ether was transferred using the call.value() method.

contract BasicDao {
	
  mapping (address => uint) public balances;
  ...
  
  //transfer the entire balance of the caller of this function to the caller
  function withdrawBalance() public {
  	bool result = msg.sender.call.value(balances[msg.sender]) ();
    if(!result) {
    	throw;
    }
  //update balance of the withdrawer
  balances[msg.sender] = 0;
  }
} 

This allowed the transfer to use the maximum possible gas limit, and also prevented the state from reverting on possible exceptions. Thus, the attackers were able to create a sequence of recursive calls to withdraw funds from the DAO using a smart contract similar to the one shown below.

contract Proxy {
	
  //Owner's address
  address public owner;
  
  //Constructs the contract and stores the owner
  constructor() public {
  	owner = msg.sender;
  }
  
  //Initiates the balance withdrawal
  function callWithdrawBalance(address _address) public {
  	BasicDAO(_address).withdrawBalance();
  }
  
  //Fallback function for this contract.
  //If the balance of this contract is less then 999999 Ether,
  //triggers another withdrawal from the DAO.
  function () public payable {
  	if (address(this).balance < 999999 ether) {
    	callWithdrawBalance(msg.sender);
    }
  }
  
  //Allows the owner to get Ether from this contract
  function drain() public {
  	owner.transfer(address(this).balance);
  }
} 

The result is the following sequence of actions (also depicted below in Figure 1):

1) Proxy smart contract will require legal withdrawal of funds.

2) Switching from BasicDAO to proxy smart contract caused a fallback function.

3) The fallback function of the proxy smart contract will request another withdrawal from BasicDAO.

4) Switching from BasicDAO to proxy smart contract caused a fallback function.

5) The fallback function of the proxy smart contract will request another withdrawal from BasicDAO.

. . .

Please note that the balance of the proxy smart contract was never updated (this happens after the transfer). Also, if the transfer to the proxy contract does not fail, the exception is never thrown and the state is never returned.

Rice.  1 Illustration of a re-entrancy attack.
Rice. 1 Illustration of a re-entrancy attack.

Prevention

The DAO contract reentry attack could have been avoided in several ways. Using the send() or transfer() functions instead of call.value() will not allow recursive withdrawal calls due to the low gas cost. Manually limiting the amount of gas passed to call.value() will have the same result.

However, there is a much simpler practice that makes any re-attack impossible. Please note that the DAO contract updates the user’s balance after the transfer of ether. If this were done prior to the transfer, any recursive withdrawal call would attempt to transfer the balance to 0 Ether. This principle applies in the general case – if after passing the ether or calling an external function inside the method there is no update of the internal state, the method is protected from the reentry vulnerability.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *