Reentrancy Attacks

What

Smart contracts will often need to call or send ether to an external address. This type of operation is inherently vulnerable to reentrancy.

To perform a reentrancy attack, the attacker deploys a malicious contract to the network. This contract intends to manipulate the logic of the target contract into sending Ether to it, thus invoking its fallback function. The fallback function then recalls the target contract during the execution, to drain the target contract of Ether.

Photo by Bret Kavanaugh on Unsplash

How

Take the following function inside a vulnerable target contract:

function withdraw() external { uint amount = balances[msg.sender]; require(msg.sender.call.value(amount)()); balances[msg.sender] = 0; }

Assuming balances is a mapping of addresses to integer values, the function performs the following actions:

Retrieve the amount of Ether to send to the caller.

of Ether to send to the caller. Send that amount to the caller

Set the balance of the caller to be zero

So long as the withdraw function is being called by a non-contract address, the logic is fine. However, if the sender is a contract address, this function can be exploited to drain the contract.

Here is a simple fallback function within our malicious contract:

function() external payable { while(calls < 10){ calls++; reentrancyContract.withdraw(); } }

Assume that calls is a state variable defined as 0 in the constructor. When the target contract attempts to send Ether to the malicious address where this contract resides, this fallback function is called. If calls is less than 10, the withdraw() function on the target contract will be called again. This repeats recursively until it is called 10 times, draining the target contract of 10 times more than intended.

This happened in real life. In 2016, the DAO hack caused huge shockwaves throughout Ethereum and the Blockchain industry. Its consequences are still being felt today.

Preventing Reentrancy Attacks

There are several ways to protect your contracts from reentrancy attack.

The first is to use transfer() to send Ether. In previous versions of Solidity, contracts needed to use the call() method, in which no gas limit was set by default. transfer() on the other hand currently has a limit of 2,300 gas units, which is about enough to emit an Event. With that limit, any complex recursive execution would run out of gas almost immediately.

Use the Checks→Effects→Interactions pattern. When writing code for sensitive operations, make sure you initially:

Run checks: require() statements. Then make the changes that affect state variables: balance -= withdrawAmount; . Then finally, perform the interaction: address.transfer(withdrawAmount) .

Another technique is to lock the contract, also known as a mutex. Use a state variable to determine if a contract function is currently being executed. If the code is locked, no function can be called until the lock is released.