Firstly, I am going to help you understand in a simple way what is a reentrancy attack and how you can prevent it, and then, I will dive deeper into code examples in order to show where are the vulnerabilities, what would be the attacker code and most importantly I will show you the latest verified methods to protect not only one but all the smart contracts in your project.
Spoiler: If you have already heard about the nonReentrant() modifier, keep reading because you are about to discover a few lines below the globalNonReentrant() modifier and the checks-effects-interactions pattern.
In the image above we have ContractA and ContractB. Now, as you know a smart contract can interact with another smart contract, like in this case, ContractA can call ContractB. So, the very basic idea of reentrancy is that ContractB is able to call back to ContractA while ContractA is still executing.
So, how can the attacker use this?
Above we have ContractA which has 10 Ethers and we see that ContractB has stored 1 Ether in ContractA. In this case, ContractB will be able to use the withdraw function from ContractA and send Ether back to itself as it passes the check where its balance is greater than 0, to then have its total balance modified to 0.
Let’s now see how can ContractB use reentrancy to exploit the withdraw function and steal all the Ethers from ContractA. Basically, the attacker is going to need two functions: attack() and fallback().
In Solidity, a fallback function is an external function with neither a name, parameters, or return values. Anyone can call a fallback function by: Calling a function that doesn’t exist inside the contract; Calling a function without passing in required data; Sending Ether without any data to the contract.
The way reentrancy works (let’s follow the arrows step by step) is with the attacker calling the attack() function which inside is calling the withdraw() function from ContractA. Inside the function, it will verify if the balance of ContractB is greater than 0 and if so it will continue the execution.
Since ContractB’s balance is greater than 0, it sends that 1 Ether back and it triggers the fallback function. Notice that at this moment ContractA has 9 Ethers and ContractB has already 1 Ether.
Next, when the fallback function gets executed it triggers again ContractA’s withdraw function, checking again if ContractB’s balance is greater than 0. If you check again the image above you will notice that its balance is still 1 Ether.
That means that the check passes and it sends another Ether to ContractB, which triggers the fallback function. Notice that since the line where we have “balance=0” never gets executed, this will continue until all Ether from ContractA is gone.
___________
Let’s now take a look at a smart contract where we can identify reentrancy with the Solidity code.
In the EtherStore contract, we have the function deposit() that stores and updates the balances of the sender and then the withdrawAll() function that will take all the balances stored at once. Please, notice the implementation of withdrawAll() where it checks first with the require that the balance is greater than 0 and right after sends the Ether, again, leaving for the end the update of the sender’s balance to 0.
Here we have the contract Attack that is going to use the reentrancy to drain the EtherStore contract. Let’s analyze its code:
In its constructor, the attacker will pass the EtherStore address in order to create an instance and so being able to use its functions.
There we see the fallback() function which is going to be called when EtherStore sends Ether to this contract. Inside it will be calling withdraw from EtherStore as long as the balance is equal or greater than 1.
And inside the attack() function we have the logic that will be exploiting EtherStore. As we can see, first we will initiate the attack by making sure we have enough ether, then deposit 1 ether in order to have a balance greater than 0 in EtherStore and hence pass the checks before starting to withdraw.
I explained above in ContractA and ContractB’s example step by step how the code will run, so now, let’s make a summary of how will it be. First of all the attacker will call attack(), which inside will call withdrawAll() from EtherStore, which then will send Ether to Attack contract’s fallback function. And there it will start the reentrancy and drain the EtherStore’s balance.
So, how can we protect our contracts from reentrancy attacks?
I am going to show you three prevention techniques to fully protect them. I am going to cover how to prevent reentrancy in a single function, reentrancy cross-function, and reentrancy cross-contract.
The first technique to protect a single function is using a modifier called noReentrant.
A modifier is a special type of function that you use to modify the behavior of other functions. Modifiers allow you to add extra conditions or functionality to a function without having to rewrite the entire function.
What we do here is lock the contract while the function is executed. This way it won’t be able to reenter the single function since it will need to go through the function’s code and then change the locked state variable to false in order to pass again the check done in the require.
___________
The second technique is by making use of the Checks-Effects-Interactions pattern which will protect our contracts from cross-function reentrancy. Can you spot in the updated EtherStore contract above what has changed?
To deep dive into Check-Effects-Interaction pattern I recommend to read https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html
Above we see the comparison between the code vulnerable from the image on the left where the balance was updated after sending the Ether, which as seen above could potentially never be reached, and on the right what it has been done is to move the balances[msg.sender] = 0 (or effect) right after the require(bal > 0) (check) but before sending ether (interaction).
This way we will be making sure that even if another function is accessing withdrawAll(), this contract will be protected from the attacker because the balance will always be updated before sending the Ether.
Pattern created by https://twitter.com/GMX_IO
The third technique I am going to show you is creating the GlobalReentrancyGuard contract to protect from cross-contract reentrancy. It is important to understand that this is applicable to projects with multiple contracts interacting with each other.
The idea here is the same as in the noReentrant modifier I have explained in the first technique, it enters the modifier, updates a variable to lock the contract and it doesn’t unlock it until it doesn’t finish the code. The big difference here is that we are using a variable stored in a separate contract which is used as the place to check if the function was entered or not.
I have created here an example without actual code and just with function names for reference to understand the idea as, from my experience, it can help visualize the situation more than just writing it with words.
Here, the attacker would be calling the function in the ScheduledTransfer contract which after meeting the conditions it would send the specified Ether to the AttackTransfer contract which would, therefore, enter the fallback function and hence “cancel” the transaction from the ScheduledTransfer contract’s point of view and yet receive the Ether. And this way it would be starting a look until draining all Ethers from ScheduledTransfer.
Well, using the GlobalReentrancyGuard I have mentioned above it will avoid such an attack scenario.
__________________
Twitter @TheBlockChainer to find more daily updates about Smart Contracts, Web3 Security, Solidity, Auditing smart contracts, and more.
__________________