Hacking The Hackers: Hands-On-Examples Of Reentrancy Attack Mitigation

Ashish MeenaAshish Meena
7 min read

INTRODUCTION

Hello everyone I'm back with a new article. In this article, I am going to explain about most famous and dangerous attack the re-entrancy attack in easy and simple wording.

DEFINITION

A Re-Entrancy attack is a type of security vulnerability that can occur in computer programs, particularly in the context of smart contracts on blockchain platforms. The attack takes advantage of the flow in the design or implementation of a program that allows an attacker to repeatedly re-enter or re-execute a specific portion of code within a contract.

UNDERSTAND BY EXAMPLE

I know by reading the definition you didn't get what I'm saying, right? Let me explain this with more clarity. Suppose there is a smart contract that contains a function that allows users to withdraw funds from their account balance. When a user requests a withdrawal, the contract deducts the requested amount from the user's balance and transfers it to the user's designated address. Now, let's say the contract also includes a callback function that gets triggered after the funds are transferred. This callback function might execute some external code or call other contracts. An attacker can take advantage of this callback mechanism to execute a malicious contract. The attacker first calls the withdrawal function but provides a malicious contract as the callback address. The withdrawal function deducts the requested amount from the attacker's balance and transfers it to the malicious contract. Instead of immediately returning to complete the withdrawal, the malicious contract's code exploits the flow in the contract's design, causing the withdrawal function to be called again. Since the attacker still has control over the malicious contract the withdrawal function is executed again, deducting more funds and transferring them to the malicious contract. This cycle continues allowing the attacker to repeatedly drain the contract's balance, often until it runs out of funds or until some other mechanism stops the attack.

CODING EXAMPLE

I know a lot of theories but, steal some confusion is running inside your mind, right? Don't worry when you see the actual code example all your doubts are clear. First, let's see the code of the contract that is transferring funds to the users.

//SPDX-License-Identifier: MIT
pragma solidity > 0.5.0 < 0.9.0;

contract reenter{
    mapping(address=>uint) public balance;
    function addEth() public payable {
        balance[msg.sender] += msg.value;
    }
    function withdrawEth() public {
        require(balance[msg.sender] != 0, "balance is zero");
        payable(msg.sender).transfer(balance[msg.sender]);
        balance[msg.sender] = 0;
    }
}

So, the above contract has a security issue that is a re-entrancy attack. When you see the code I know you don't get the problem. Don't worry I will explain the problem as well as the solution for this code. So first, let's understand the flow of this code what this code is doing and how. In the first, we declared a mapping that holds the balance information of the particular address like address "0xabc" which has 10 ethers or 100 ethers.

After this we created a function named "addEth" Through this function we are sending ethers to the contract because the function is payable that's why the contract can receive the ethers and through this "balance[msg. sender] += msg. value;" line we are adding the received value to the caller of the "addEth" function. In the next function named "withdrawEth," the contract sends back the received amount from the caller. First, the contract transfers the amount through this "payable(msg. sender).transfer(balance[msg. sender]);" line and when the caller gets the transferred amount the contract converts the balance of the caller to zero through this "balance[msg. sender] = 0;" line. Here is the main problem that caused the re-entrancy attack, How? When this function is called through metamask or other wallet then there is no problem but when other contracts call this contract function through inner call(about this we will discuss in another article) then the problem comes. Let's first see the contract function that calls our contract "withdrawEth" function. I'm not going to show you the whole contract code but I will show only the function that calls our "withdrawEth" function.

    receive() external payable{
        withdrawEth();
    }

The contract uses the "receive" function to call our contract function but why does the contract use the receive function? I will tell you the reason but we will discuss all about receive as well as the fallback function in the next article. Through the payable keyword in the receive function contract can receive the ethers as I told you earlier. Why does the receive function have external visibility? We will discuss it in the next article so stay tuned. So let's move on to the flow of this because by understanding the flow most of the doubts are cleared. The flow is like first the owner of this malicious contract invests some ethers by calling the addEth function because if the contract does not add ethers then it is not able to call the withdrawEth function because of the required statement in the withdrawEth function. You can understand the required statement as an if-else condition because if the condition meets then the line of code moves forward otherwise it reverts the error message that is given in the required statement in the string format. As the malicious contract has already added ethers the condition is met then it transfers the amount to the contract and the contract receives that amount because of a payable keyword but the problem comes here because as you can see the receive function calls the withdrawEth function so as per our thinking after the completion of the transaction the contract need to come back and convert the balance of caller to zero through the below line in the contract function but the malicious contract break the flow of our contract and call the withdrawEth function again and then our contract transfer ones again because the balance of the caller is not converted to zero so the required condition meets and this flow keeps going until all funds did not become zero of our contract. You can understand it like when function A runs its code and calls the other function B and function B calls the other function C and function C calls other function D. So when function D is complete then it comes back to function C and function C complete its execution and go back to function B and function B complete its execution and the flow come back to the main function A and after completing all the functions the function A get the time to complete it's execution. This is how the execution is complete but if function D calls again function A then the flow of the code is kept in the circular loop and run again and again, all functions call each other and none of any function is complete.

function A(){
    B();
}
function B(){
    C();
}
function C(){
    D();
}
function D(){
    A();
}

PROBLEM BY REENTRANCY

The vulnerability arises from the fact that the contract doesn't properly account for re-entrancy scenarios, allowing the attacker to exploit the code's control flow. Re-entrancy attack can lead to significant financial losses or disruption of the affected system.

PREVENTION

Now you understand about reentrancy attack but you thinking about how we can protect our code from this attack. Don't worry as I promised you I will provide you with the solution to this problem also, here is the solution. We just need to make changes to our contract withdrawEth function. Let's see.

    function withdrawEth() public {
        require(balance[msg.sender] != 0, "balance is zero");
        uint temp = balance[msg.sender];
        balance[msg.sender] = 0;
        payable(msg.sender).transfer(temp);
    }

The above is the change that I'm talking about. In these changes I create one local variable named "temp" that holds the balance of the caller after that we convert the balance to zero and in the next line, we simply transfer the temp amount to the caller. If the malicious contract calls the withdrawEth function once again then the required condition is false because we convert the balance of the caller to zero. That's why the transaction reverted and printed the error message that we gave in the required function. This is how we can prevent our code from reentrancy attacks. Whenever you write your contract code then make sure the position of every line of code. This is all about a reentrancy attack I hope all your doubts are cleared and you understand about it.

To prevent re-entrancy attacks, developers must carefully design and implement their code to ensure that proper checks and safeguards are in place. Techniques such as using mutex locks or employing a "checks-effects-interactions" pattern can help mitigate the risk of re-entrancy vulnerabilities. Additionally, conducting thorough security audits and testing can help identify and address such vulnerabilities before deploying the code in production.

Thank you for reading this article, Keep reading, Keep learning and Keep growing.

0
Subscribe to my newsletter

Read articles from Ashish Meena directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashish Meena
Ashish Meena