Reentrancy attacks are a serious vulnerability that occur in smart contracts, and are becoming increasingly popular in decentralized finance (DeFi). The infamous “The DAO” incident, where a hacker exploited a vulnerability in a smart contract and drained approximately $50 million worth of Ether, brought the issue of reentrancy attacks to the forefront.
In this blog post, we’ll explore what reentrancy attacks are, how they work, and what can be done to prevent them.
What is Reentrancy Attack?
Reentrancy can occur when a program or contract makes a call to another program or contract and then continues to execute. Let’s understand it with a simple example.
Imagine you have an online bank account and you want to transfer money to your friend’s account. Your account has $100 and your friend’s account has $0. Your account has a function which can be called to transfer money to other accounts. Once you transfer the money, the function updates the balance in your account and your friend’s account.
Now, imagine a hacker found a vulnerability in the code of the function that allows the hacker to call the function multiple times before the balance is updated. So, when you transfer $10 to your friend’s account, the hacker can call the function multiple times, each time taking $10 from your account.
So, even though you transferred $10, the hacker can take $100 from your account by repeatedly calling the function before the balance is updated. This is a reentrancy attack, where the hacker repeatedly calls the transfer function, draining all of the account’s resources.
Let us look at an example from Ethernaut CTF which can be found at the ethernaut page (look below for the link)
Here, on line number 12 inside the donate function, contract will update the balance to the amount we donate.
On line 20 inside withdraw function, the contract checks whether the amount we are withdrawing is equal or less than our balance, if it is true then it will send ether out from the contract to external address on line 21 and then update the balance at line 25.
This is example of a classic Reentrancy attack. An attacker can recursively call the withdraw function and since the state changes are been made after the transfer, the condition that it checks on line 20 will always be true and we can drain out entire balance from the contract.
Let us look how the malicious or attacker’s contract will look like, again the code can be found below
Here, we import the vulnerable contract on line 3, on line 12 inside the constructor we initialize the imported contract.
Now as we saw earlier, before withdrawing any amount, the contract will check if we have sufficient balance and that could be updated using the donate function. So on line 17 inside the attack function we first donate 1 ether ( 1e18 = 1×10^18) to the contract to update the balance and then on next line we will call the withdraw function which will send the 1 ether that we donated to us.
Upon receiving 1 ether, it will trigger the receive function. Inside receive function we check if the balance is greater than 0 to prevent infinite loop problem and then call the withdraw function again.
By, doing this we will repeatedly call the withdraw function of vulnerable contract until the contract’s balance is 0. We can do this because contract is updating our balance after sending out the ether to external address ( attacker controlled ). So, after receiving the ether that was sent to us, it will trigger the receive function which is a fallback function. And we can again call the withdraw function to withdraw more than we donated. Notice line 21, in the Hack contract.
This will call the self-destruct function once the balance of vulnerable contract is drained out. Self-destruct is a special kind of function which destroys the contract ( Hack contract ) that is calling it and sends the ether it holds to the msg.sender ( this is us ). This is how Reentrancy works.
There are several ways to fix the reentrancy vulnerability in contract. It is important to note that the best approach depends on the specific use case and requirements of the contract. It is always a good idea to have a security audit done by a professional auditor to validate the implementation.
Here is just a basic approach on how you can fix the vulnerability in this contract.
We have used 2 simple techniques to fix the vulnerability:-
- By making state changes before sending out ether. This simply means that we update our balance first, and then send out ether. By doing this, even if an attacker calls back, then the condition on line 28 will be false as we updated the balance prior to sending ether, and the remaining code will not be executed
- Using a modifier that locks the contract while the function is executing. Modifier in simple words just checks for a certain condition that we specify and then executes the remaining code. One such modifier can be found on line number 12. If the attacker calls the withdraw function again, the contract will be locked as only a single function will be executed at a time
Note: This 2nd approach is based on the Reentrancy guard from Openzepplin’s contract which are basically very secure. This is just a demonstration, and it may not completely fix the issue. Getting a security audit done is highly recommended.
Payatu is a research-powered, CERT-In empaneled cybersecurity consulting company specializing in security assessments of IoT product ecosystem, Web application & Network with a proven track record of securing applications and infrastructure for customers across 20+ countries.
Want to check the security posture of your organization? Browse through Payatu’s Service and get started with the most effective cybersecurity assessments.
Have any specific requirements in mind? Let us know about them here and someone from our team will get in touch with you.