If we take a closer look into the biggest crypto hacks and the eye-watering figures lost to them, they would have deep-rooted from the coding flaws.
One such common occurrence of security vulnerability is the Reentrancy attack. However, the destructive effect caused due to mishandled reentrancy may not sound as simple as launching the attack itself.
Despite being a familiar and well-publicized issue, the appearance of the Reentrancy bug in smart contracts is always inevitable.
How often had the Reentrancy vulnerability been exploited by hackers in the past years? How does it work? How to restrain smart contracts from losing funds to Reentrancy bugs? Find answers to these questions in this blog.
So, before long, let’s brush up on the largest re-entrancy attacks in memory.
Some of The Most Infamous Real-time Reentrancy Hacks
Reentrancy attacks that caused the most devastating effects on the projects ended up doing one of these two or even both.
- Drain off the Ether completely from the smart contracts
- Hackers sneaking their way into the smart contract code
We can now observe a few cases of Reentrancy attacks and their impact.
Jun 2016: DAO attack – 3.54M or $150M Ether
Apr 2020: Uniswap/Lendf.Me hack – $25M
May 2021: The BurgerSwap hack – $7.2M
Aug 2021: CREAM FINANCE hack – $18.8M
Mar 2022: Ola Finance – $3.6M
Jul 2022: OMNI protocol – $1.43M
It is crystal clear that Reentrancy attacks have never went out-of-style. Let’s gain deep insights into it in the following passages.
Overview of Reentrancy attack
As from the name “Reentrancy”, which implies “Reentering again and again.” Reentrancy attack involves two contracts: The victim contract and the Attacker contract.
The attacker contract exploits the reentrancy vulnerability in the victim contract. It uses withdraw function to achieve it.
The attacker contract calls the withdraw function to drain the funds from the victim contract by making repeated calls before the balance in the victim contract is updated. The victim contract will check the balance, send funds and update the balance.
But within the time frame of sending the funds and updating the balance in the contract, the attacker contract makes the continuous call to withdraw funds. As a result, the balance is not updated in the victim contract until the attacker contract drains off all the funds.
The severity and the cost of reentrancy exploitation alarm the dire need for performing smart contract audits to rule out the possibility of overlooking such errors.
Illustrative view of Reentrancy Attack
Let’s fathom the concept of reentrancy attack from the simplified illustration below.
Here are two contracts: The vulnerable contract and the Hacker contract
The hacker contract makes a call to withdraw from the vulnerable contract. On receiving the call, the vulnerable contract checks for the funds in the hacker contract and then transfers the funds to the hacker.
The hacker receives the funds and implements the fallback function, which calls again into the vulnerable contract even before the balance is updated in the vulnerable contract. Thus repeating the same operation, the hacker withdraws the funds completely from the vulnerable contract.
Features Of Fallback Function Used By Attacker
- They are externally callable. I.e. they cannot be called from within the contract they are written
- Unnamed function
- The fallback function doesn’t include arbitrary logic inside it
- Fallback is triggered when ETH is sent to its enclosing smart contract, and no receive() function is declared.
Analysing Reentrancy Attack From A Technical View
Let’s take a sample contract and understand how the reentrancy attack occurs.
contract Attack DepositFunds public depositFunds; constructor(address _depositFundsAddress) depositFunds = DepositFunds(_depositFundsAddress); // Fallback is called when DepositFunds sends Ether to this contract. fallback() external payable if (address(depositFunds).balance >= 1 ether) depositFunds.withdraw(); function attack() external payable require(msg.value >= 1 ether); depositFunds.depositvalue: 1 ether(); depositFunds.withdraw();
This is the attacker contract wherein the attacker deposits 2ETH. The attacker calls the withdraw function in the vulnerable contract. Once the funds are received from the vulnerable contract, the fallback function is triggered.
The fallback then executes the withdraw function and drains the fund from the vulnerable contract. This cycle goes on until the funds are completely exhausted from the vulnerable contract.
contract DepositFunds mapping(address => uint) public balances; function deposit() public payable balances[msg.sender] += msg.value; function withdraw() public uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.callvalue: bal(""); require(sent, "Failed to send Ether"); balances[msg.sender] = 0;
The vulnerable contract has 30ETH. Herein the withdraw() function sends the requested amount to the attacker. Since the balance is not updated, the tokens are transferred to the attacker repeatedly.
Types Of Reentrancy Attacks
- Single function reentrancy
function withdraw() external uint256 amount = balances[msg.sender]; require(msg.sender.call.value(amount)()); balances[msg.sender] = 0;
The msg.sender.call.value(amount)() transfers the funds after which the attacker contract fallback function calls withdraw()again before the balances[msg.sender] = 0 is updated.
- Cross-function Reentrancy
function transfer(address to, uint amount) external if (balances[msg.sender] >= amount) balances[to] += amount; balances[msg.sender] -= amount; function withdraw() external uint256 amount = balances[msg.sender]; require(msg.sender.call.value(amount)()); balances[msg.sender] = 0;
Cross-function reentrancy is way more complex to identify. The difference here is that the fallback function calls transfer, unlike in single-function reentrancy, where it calls withdraw.
Prevention Against Reentrancy Attacks
Checks-Effects-Interactions Pattern: Checks-effects-interactions pattern helps in structuring the functions.
The program should be coded in a way that checks the conditions first. Once passing the checks, the effects on the contracts’ state should be resolved, after which the external functions can be called.
function withdraw() external uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; require(msg.sender.call.value(amount)());
The rewritten code here follows the checks-effects-interactions pattern. Here the balance is made zero before making an external call.
Use of modifier
The modifier noReentrant applied to the function ensures there are no reentrant calls.
contract ReEntrancyGuard bool internal locked; modifier noReentrant() require(!locked, "No re-entrancy"); locked = true; _; locked = false;
In The End
The most effective step is to take up smart contract audits from a leading security firm like QuillAudits, wherein the auditors keep a close eye on the structure of the code and check how the fallback function performs. Based on the studied patterns, recommendations are given to restructuring the code if there seem to be any vulnerable behaviours.
Safety of funds is ensured right before falling victim to any losses.
What is a reentrancy attack?
A reentrancy attack happens when a function in the vulnerable contract makes a call to an untrusted contract. The untrusted contract will be the attacker’s contract making recursive calls to the vulnerable contract until the funds are completely drained off.
What is a reentrant?
The act of re-entering means interrupting the execution of the code and initiating the process all over again, which is also known as re-entering.
What is a reentrancy guard?
Reentrancy guard uses a modifier which prevents the function from being called repeatedly. Read the blog above to find the example for reentrancy guard.
What are some of the attacks on smart contracts?
Smart contracts are exposed to numerous vulnerabilities, such as reentrancy, timestamp dependence, arithmetic overflows, DoS attacks, and so on. Therefore, auditing is a must to ensure there are no bugs that collapse the logic of the contract.