Reentrancy Attack in Smart Contracts

Part1: Mono-Function Reentrancy

ChainWall
ChainWall
9 min readNov 1, 2023

--

Introduction to Reentrancy Attacks

In the world of blockchain technology, smart contracts have gained significant popularity due to their ability to automate and enforce agreements without the need for intermediaries. However, as smart contracts handle valuable assets and execute complex operations, they are vulnerable to various types of attacks. One such vulnerability is Reentrancy, which can pose significant risks to the security and functionality of smart contracts. A Reentrancy attack occurs when a function in a contract makes an external call to a malicious contract without properly accounting for potential reentrant calls. This vulnerability allows the malicious contract to repeatedly invoke the vulnerable function before the previous execution completes, potentially leading to unexpected behaviors and enabling the attacker to manipulate the vulnerable contract’s state or completely drain its funds.

When we examine the history of attacks on smart contracts, it becomes clear that one of the oldest and most destructive types of attacks is Reentrancy, which remains an ongoing concern. In 2016, the notorious DAO hack brought this attack into the spotlight. The DAO was designed as an investment fund governed by a smart contract, allowing network members to vote on investment decisions directly based on the number of tokens they held. During the DAO hack, approximately $60 million worth of Ether was stolen from the smart contract. In response to this incident, the Ethereum community decided to fork the blockchain to reverse the effects of a malicious transaction that exploited this Reentrancy vulnerability. It has been over 7 years, but the fundamental nature of Reentrancy attacks on smart contracts has not changed significantly. While technology and security measures have evolved, the risk of Reentrancy attacks continues to loom over smart contracts as a potential threat. To understand the impact of Reentrancy vulnerabilities, let’s explore a few notable real-world examples:

  • Parity Wallet Hack (November 2017): A Reentrancy vulnerability was discovered in the Parity multi-signature wallet contract, and exploiting this vulnerability resulted in the loss of approximately $30 million in funds.
  • Lendf.Me Attack (April 2020): An attacker executed a Reentrancy attack to steal $25 million from Lendf.me. This platform operates within the decentralized finance (DeFi) ecosystem, focusing on lending activities on the Ethereum network.
  • Cream Finance hack (October 2021): An attacker managed to steal over $130 million worth of ERC-20 and CREAM liquidity protocol (LP) tokens by exploiting a Reentrancy vulnerability in the protocol’s “‘flash loan” feature.
  • Fei Protocol Incident (April 2022): An attacker drained approximately $80 million in tokens from a vulnerable contract. This security breach was rooted in a Reentrancy vulnerability, allowing the attacker to borrow assets while simultaneously withdrawing all the collateral that had been deposited.
  • dForce DeFi Protocol hack (February 2023): An attack was launched against the dForce protocol, specifically targeting the Curve Finance vault on the Arbitrum and Optimism blockchains. The attacker exploited a Reentrancy vulnerability, resulting in an estimated theft of $3.6 million in assets from the protocol.

Reentrancy attacks in solidity can be classified into the three following types:

1. Mono-Function Reentrancy: This type of Reentrancy occurs when the vulnerable function is the same function that is repeatedly called by the attacker, before the completion of its previous invocations. It is a simpler and more easily detectable form of Reentrancy attack compared to the other two types.

2. Cross-Function Reentrancy: This type of Reentrancy is similar to the Mono-Function Reentrancy, except the function reentered is not the same as the one making the external call. This type of attack is only possible when a vulnerable function shares its state with another function, resulting in an advantageous outcome for the attacker.

3. Cross-Contract Reentrancy: This type of Reentrancy attack takes place when the state of one contract is invoked in another contract before it’s fully updated. It often occurs when multiple contracts manually share a common state variable, and some of them update it in an insecure manner.

In this article, we will explore Mono-Function Reentrancy, its implications, and preventive measures to mitigate its risks. In upcoming articles, we will introduce two other types of vulnerabilities.

It is reminded that this article has only an educational aspect, and refrain from using vulnerable codes in your programs or exploit codes for attacks.

How Does Mono-Function Reentrancy Attack Work?

A Mono-Function Reentrancy attack occurs when a malicious contract or attacker repeatedly calls a specific function within your contract, exploiting a vulnerability within that function. This vulnerable function is susceptible to repeated recursive invocations before the completion of previous invocations. A Mono-Function Reentrancy attack involves two smart contracts: A vulnerable contract and a malicious contract. To facilitate a better understanding of this attack, we at first introduce two sample contracts, ChainWallet and ChainReentrancy and then describe prevalent attack scenario. Below, you will find the general characteristics of these two contracts along with their code.

The ChainWallet contract serves as an example of a vulnerable contract susceptible to Reentrancy attacks:

  • Users interact with this contract by depositing and withdrawing funds.
  • It contains a vulnerable function called withdraw that makes an external call.
  • It maintains a balance for each user.

The ChainReentrancy contract represents the attacker’s contract or Malicious contract:

  • The attacker deploys this malicious contract on the same blockchain network as ChainWallet to exploit ChainWallet’s vulnerability.
  • It has a receive function that is triggered when the ChainWallet contract sends the tokens to it.
  • It has an attack function that can call the withdraw and deposit functions in the ChainWallet contract.

Mono-Function Reentrancy Attack Scenario

Certainly, let’s continue to provide a detailed explanation of a Mono-Function Reentrancy attack scenario. To fully understand the scenario, please carefully follow the diagram below and review the code for the two sample contracts mentioned above: ChainWallet and ChainReentrancy. In the following explanation, we will use “CW” as an abbreviation for “ChainWallet” and “CR” as an abbreviation for “ChainReentrancy” for easier reference to each line of code in these two smart contracts.

Step 1: Depositing Ether to the ChainWallet Contract by Users

Let’s begin by considering the ChainWallet contract with no initial balance. Subsequently, three users — Alice, Bob, and John — execute the deposit function (CW:Line 8), each contributing 1 Ether to the ChainWallet contract. As a result, this vulnerable smart contract now holds a total of 3 Ether. The balance of this contract can be viewed using the getBalance function (CW:Line 26). Based on the defined balances mapping (CW:Line 6) and the getUserBalance function (CW:Line30) , the balances for each user within this contract are as follows:

· balances [Alice] = 1 Ether
· balances [Bob] = 1 Ether
· balances [John] = 1 Ether
· balances [attacker] =0 Ether

Step 2: Initiating the Reentrancy Attack by Invoking the attack Function

To initiate the exploitation of the Reentrancy vulnerability in the ChainWallet contract, the attacker invokes the attackfunction within the ChainReentrancy contract (CR: Line 21).

Step 3: Depositing Ether to the ChainWallet Contract by Attacker

Since only someone who has previously deposited some Ether into the ChainWallet contract can withdraw their balance from it, an attacker must deposit, for example, 1 Ether into this vulnerable contract to initiate an attack (CR: Line 22). By invoking the deposit function within the ChainWallet contract (CR: Line 23) and transferring 1 Ether to it, its balance increases to 4 Ether, and the attacker’s balance in this contract will also become equal to 1 Ether.

· balances [Alice] = 1 Ether
· balances [Bob] = 1 Ether
· balances [John] = 1 Ether
· balances [attacker] =1 Ether
· balance (ChainWallet) = 4 Ether
· balance (ChainReentrancy) = 0 Ether

Step 4: Withdrawal of Ether from ChainWallet Contract

Following the execution of the attack function within the ChainReentrancy contract, the withdraw function is immediately called on the vulnerable ChainWallet contract to extract the 1 Ether that the attacker had initially deposited (CR: Line 24).

Step 5: Transferring Ether to ChainReentrancy Contract

In the ChainWallet contract, there is a check to determine if the attacker’s stored balance is greater than 0 (CW: Line 16). Since the attacker had previously transferred 1 Ether in step 3, this condition is satisfied, and 1 Ether is transferred to the ChainReentrancy contract (CW: Line 19). However, because the execution of the send transaction waits for the ChainReentrancy’s receive function to complete, the attacker’s balance in the contract does not get updated to 0 and remains at 1 Ether until receive function finishes. In fact, CW:line 23 is not executed.

Step 6: Re-withdrawal of Ether from the ChainWallet Contract

Upon receiving a transfer, the receive function is triggered (CR: Line 15). At this point, the ReentrancyChain’s balance will be 1 Ether, and the ChainWallet’s balance will be 3 Ethers. Then in receive function is checked whether the balance in the vulnerable ChainWallet contract is greater than or equal to 1 Ether (CR: Line 16). If this condition is true, withdraw function is invoked once again on the ChainWallet contract (CR: Line 17). Currently, the ChainWallet’s balance is 3 Ethers, which satisfies this condition and leads to the invocation of withdraw function once more. It is precisely this vulnerability that allows the malicious ChainReentrancy contract to call the vulnerable withdraw function before completing its previous execution and updating the attacker’s balances.

Repeat Steps 5 and 6: Performing Multiple Reentrants

ChainWallet contract permits another withdrawal due to the attacker’s balance not being updated yet. Therefore, 1 Ether is transferred to the malicious ChainReentrancy contract, and steps 5 and 6 are repeated over and over until the attacker drains all the funds stored in the vulnerable ChainWallet contract and finally, the attacker’s balance in the ChainWallet contract is updated. Eventually, the balances of the entities will appear as follows:

· balances [Alice] = 0 eth
· balances [Bob] = 0 eth
· balances [John] = 0 eth
· balances [attacker] =0 eth
· balance (chainwallet) = 0 eth
· balance (ChainReentrancy) = 4 eth

How to Protect Smart Contract Against a Mono-Function Reentrancy Attack?

To prevent Mono-Function Reentrancy attack, you can consider the following solutions:

Use the checks-effects-interactions pattern

Prior to initiating any external calls or interacting with other contracts, it is crucial to perform initial checks. If these checks pass, the function should then resolve all the effects to the state of the contract. The function should only engage in interactions with external contracts once all state changes have been successfully executed.

Let’s rewrite our vulnerable withdraw function in ChainWallet contract using the checks-effects-interactions pattern. The CEIFixedChainWallet contract is the fixed version of the ChainWallet.

As you can see in the code above, in the CEIFixedChainWallet contract, withdraw function has been enhanced to follow the checks-effects-interactions pattern. This improvement was made by relocating the effect part (balances[msg.sender] = 0) to occur before the interaction part (msg.sender.call{value: balance}(“”)). This coding pattern guarantees that the attacker’s balance is updated before returning Ether to them.

Use the Mutex/Reentrancy guard

Another common practice is to use the Mutex pattern, also known as Reentrancy Guard. This method leverages the use of modifiers to lock the contract and ensure that a given function cannot be reentered until it finishes its execution, and the contract is unlocked.

Let’s integrate this mutex locking mechanism into our ChainWallet contract’s code. The MutexFixedChainWallet contract is a fixed version of the ChainWallet.

To implement mutex locking, first, we’ve created a boolean variable called locked (Line 8) and the noReentrant modifier (lines 10–15), then added the noReentrant modifier to withdraw function at line 22. The noReentrant modifier acts as a simple lock, allowing only one entry to the function where it’s applied. If there is an attempt to execute a reentrant call, the transaction will be reverted by require statement in line 11.

Another approach to implement this locking mechanism is using a Reentrancy guard library like OpenZeppelin’s ReentrancyGuard. This library features a modifier called “nonReentrant” that serves as a safeguard for functions. Inheriting from ReentrancyGuard will make the nonReentrant modifier available, which can be applied to functions to make sure there are no nested (reentrant) calls to them.

Conclusion

In this article, we have delved into the concept of Mono-Function Reentrancy attack, explored its implications, and discussed preventive measures to mitigate the associated risks. To illustrate these points, we have presented two code examples: one illustrating a vulnerable smart contract and another demonstrating an exploiting contract. Additionally, we’ve provided a detailed description of a Mono-Function Reentrancy attack scenario. In our upcoming articles, we will continue our exploration by introducing and examining two other types of Reentrancy attacks: Cross-Function Reentrancy and Cross-Contract Reentrancy. Stay tuned for more insights into smart contract security.

--

--