3 Ways to Prevent Reentrancy Attacks in Smart Contract Development

Written by dansierrasam79 | Published 2023/01/19
Tech Story Tags: web3 | blockchain | blockchain-technology | ethereum | solidity | smart-contracts | reentrancy-attack | web3-development

TLDRSmart Contract Reentrancy Attacks are the latest in a growing list of smart contract bugs. These bugs can be exploited between two smart contracts, and can lead to the loss of millions in crypto. Even though these attacks tend to grow in complexity, there are fixes that can make the next hack by exploiting this vulnerability all that more difficult. Smart contract bugs also have types or classifications ranging from logic and data to performance and security, all have their own cause, consequence, detection criteria and severity level.via the TL;DR App

Smart contracts. We’ve heard so much about how these ‘classes’ of code have the potential to change the world. In fact, it’s the very reason why Ethereum is where it is today, in being able to go beyond what Bitcoin had to offer.

As exciting as this innovation was, there were certain caveats that were worth taking into account when looking at blockchain technology as a trend. Much like any unit of code written, there’s every chance that the existence of software bugs could rudely shove blockchain technology adoption right from the peak of inflated expectations all the way down to the trough of disillusionment.

Speaking of which, even if the blockchain itself cannot be hacked in a conventional sense of the term, there’s still a distinct possibility of losing millions in crypto, thanks to the existence of smart contract bugs.

Common Smart Contract Bugs

Software bugs, in general, tend to make themselves apparent when unexpected results or behavior occur.

Now, there are several types of bugs that one might experience when using said software: some of them include functional, logical, workflow, and unit-level bugs among other types. Given how detrimental this can prove to be to the user experience, testing often occurs in order to resolve these bugs.

Not very differently, smart contract bugs also have types or classifications ranging from logic and data to performance and security, all of which have their own cause, consequence, detection criteria, and severity level. Also, as a result, they also fail to meet functionality, performance, security, and serviceability requirements.

Among those with a critical severity level, the reentrancy bug causes great damage, as it results in the loss of millions in crypto and which has occurred as recently as 2021.

That said, even though these attacks tend to grow in complexity, there are fixes that can make the next hack attempt much more difficult.

Your First Look at Smart Contract Reentrancy Attacks

Just about any English dictionary will give you the meaning of reentrancy, which can be simplified to the term ‘to reenter’.

In the context of computer science, there are certain functions that can be reentered repeatedly in a safe manner despite being interrupted in the case of a single-processor system or can run concurrently on several processors.

However, in the case of smart contracts, reentering a function repeatedly can spell disaster, as we will discover. Before we begin explaining how this vulnerability is exploited, a couple of things must be established: reentrancy usually occurs between two smart contracts, where, for the sake of simplicity, contract A is the vulnerable contract and contract B is malicious. Second, whenever contract function calls are made, the Ethereum Virtual Machine (EVM) transfers execution from the calling contract to the one that is being called, from contract A to B.

Third, this is a critical time since the called contract — Contract B, in the diagram above— can now make calls of its own and which can prove to be risky if updates to the contract’s state haven’t been made. This, in a nutshell, is what occurs when the reentrancy vulnerability is exploited by contract B.

Why Are Smart Contract Reentrancy Attacks So Devastating

Even if these smart contract reentrancy attacks continue to diminish in severity, a substantial amount of money has been lost.

Probably the most prominent of these attacks involve the DAO Hack that took place in 2016, which raised more than $150 million from a pool of 11,000 investors. As you know, a decentralized autonomous organization (DAO) consists of code written in the form of smart contracts that enforce the rules, guidelines, and functionalities of said organization. It should be pointed out that the need for a centralized authority is eliminated too.

Now, as with other smart contracts, one written for a DAO also cannot be altered once it is deployed to the blockchain. In the case of the DAO hack, the ability to exploit vulnerabilities in the code was already observed by experts and became a reality on June 17, 2016.

By means of several recursive calls, the hacker was able to drain $60 million worth of Ether but this was because of the flaw where the transfer of Ether took place before the balance was updated in the contract itself.

If you look at the diagram above, the Ether was transferred by making repeated withdrawals from a fallback function. As we move on to the next section that involves going over Solidity code, we’ll get to see this in greater detail.

How Reentrancy Attacks Occur Using Solidity Code

Now, let’s look at how reentrancy attacks occur using Solidity code:

The Ether Store contract has three functions namely deposit, withdraw, and getBalance. As is evident from the function definitions, the first makes a certain deposit for said address, the second withdraws a balance based on the address while the getBalance function returns the balance of the contract.

As you can tell, the Attack contract is the one that performs the hack where an initial amount of 1 ether is deposited to the smart contract. This is so that the line “require(bal>0)” check can be satisfied before the attack begins to make numerous withdrawals via the fallback function.

Since the balance of the sender isn’t yet set to zero as in the line “balances[msg.sender] = 0”, the attacker can continue to receive Ether via the fallback function. As the attacker can make recursive calls to the withdraw function Ether Store contract, it’s very possible that they can continue to do so until the funds in the contract are completely drained.

For example, let us deploy both contracts to the Remix IDE, and add an arbitrary value of 6 Test Ether to the EtherStore contract, as shown below:

Now, let’s run the Attack contract, but by first depositing 1 Ether, and then calling the withdraw() function, as shown below:

As you can see in the screenshot below, the entire balance from the EtherStore contract has been transferred to the Attack contract, and which is why a balance of 7 Ether is in the contract.

That said, one must remember that this does not mean that there’s a flaw with the Ethereum blockchain or even with the Solidity programming language. Instead, the DAO Hack took place due to the oversight of the smart contract programmer and which resulted in the Ethereum blockchain being broken up into two separate chains, with the attackers still making off with a significant amount of funds.

3 Ways to Fix Smart Contract Reentrancy Errors

Now, there are three ways by which one can fix this reentrancy error: using the checks-effects-interactions pattern, using a non-reentrancy guard offered by the Open Zeppelin library or by using an intermediary escrow address.

Now, when we implement the checks-effects-interactions pattern, we prevent the withdraw function in the contract above from being accessed. At least, not unless the associated ‘state’ with that call of the withdraw function has been changed.

Even if it’s usually best to wait until the external interaction has taken place successfully, the pattern requires the state to be updated prior to continuing with the external interaction. In other words, the balance is reduced prior to completing the withdrawal, as shown below:

This is why applying the checks-effects-interactions pattern is considered to be counter-intuitive. Still, with the state changed, this prevents reentrancy and is thus considered to be one such solution.

Another solution requires using the Open Zeppelin framework to add a mutex lock. Specifically, you would use the nonReentrancy modifier as found in the ReentrancyGuard.sol contract at this link to ensure that said contract isn’t called over and over again, either directly or indirectly.

Here’s how you would need to rewrite the withdraw function to accommodate such a mutex lock using the OpenZeppelin library and the nonReentrant modifier:

If you intend to use this code, replace the text addContractLink in the above import statement with the following link within double quotes.

The third and final fix involves using an intermediary escrow address that keeps the funds withdrawn from the contract. The receiver can pull payment but only once since an escrow prevents contract reentrancy. Of course, if the escrow holds several accounts, there is a chance of reentrancy taking place, but this can be prevented using the check-effects-interactions pattern as well as the reentrancy guard methods described above.

Now, even if these three fixes should work, it’s well worth noting that this reentrancy issue does not crop up if you use a blockchain like Aeternity since its smart contract programming language Sophia, by virtue of adopting the functional programming paradigm, prevents such a condition in the first place.

Are Smart Contract Reentrancy Attacks Limited to Ethereum?

Ever since this type of bug caused the DAO Hack on the Ethereum blockchain, other protocols have taken steps to prevent such an error from occurring on theirs.

So, while it’s entirely possible to consider this bug to be no longer harmful, there have been reentrancy attacks as recently as 2021. These attacks have also resulted in the loss of millions too, so these shouldn’t be taken lightly.

But yes, these attacks are primarily limited to the Ethereum blockchain and which is why we focused on providing Solidity code for the fixes too. That said, since most of the chains are EVM compatible, it only makes sense that developers should stay aware of such persistent bugs and their fixes too.

Also published here.


Written by dansierrasam79 | Loves emerging tech, languages such as Python, JavaScript, Solidity & Haskell. Writes about Web3. Works at Lumos Labs.
Published by HackerNoon on 2023/01/19