Mystery Box

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

Owner Withdrawal Drains Funds, Preventing User Reward Claims

Summary

The MysteryBox contract allows the owner to withdraw all available funds through the withdrawFunds() function, without reserving any Ether to cover user rewards. As a result, users may receive valuable rewards (such as Gold or Silver Coins) without the contract having sufficient funds to pay out these rewards. This Ether mismanagement can lead to a situation where users are unable to claim their rewards, causing financial loss and damaging trust in the protocol.

Vulnerability Details

The withdrawFunds() function enables the owner to withdraw the entire Ether balance of the contract:

function withdrawFunds() public {
require(msg.sender == owner, "Only owner can withdraw");
(bool success,) = payable(owner).call{value: address(this).balance}("");
require(success, "Transfer failed");
}

This function does not account for any Ether that needs to be reserved to pay for rewards that users have won, such as Gold and Silver Coins. By allowing the owner to withdraw all funds, the contract can be left without the necessary Ether to pay out these rewards. Users who have earned high-value rewards may not be able to claim them, resulting in financial instability for the contract.

Additionally, there is no separation between the funds needed to pay rewards and the funds that the owner can withdraw, which increases the risk of the contract being drained.

Impact

  • Financial Loss for Users: Users may receive rewards that the contract is unable to pay, resulting in financial losses.

  • Loss of Trust in the Protocol: If users are unable to claim rewards, they will lose trust in the system, which could lead to a reduction in engagement and participation.

  • Depletion of Contract Funds: The contract’s funds could be completely drained, leaving it unable to operate or fulfill its reward obligations.

Tools Used

Manual code review

Recommendations

  • Implement a Prize Pool Reserve:
    Introduce a mechanism to reserve a portion of the contract’s Ether balance specifically for paying user rewards. This can be achieved by setting aside a percentage of the Ether received from box purchases for the prize pool and preventing the owner from withdrawing these funds.

Example:

uint256 public prizePoolBalance;
function buyBox() public payable {
require(msg.value == boxPrice, "Incorrect ETH sent");
boxesOwned[msg.sender] += 1;
prizePoolBalance += msg.value * 80 / 100; // Allocate a percentage to the prize pool
}
function withdrawFunds() public {
require(msg.sender == owner, "Only owner can withdraw");
uint256 availableBalance = address(this).balance - prizePoolBalance; // Ensure funds are reserved for rewards
require(availableBalance > 0, "No funds available for withdrawal");
(bool success,) = payable(owner).call{value: availableBalance}("");
require(success, "Transfer failed");
}
  • Restrict Full Balance Withdrawals:
    Limit the amount the owner can withdraw to ensure that enough funds remain to cover outstanding rewards. The withdrawal function should only allow the owner to withdraw funds that are not reserved for user rewards.

  • Track Reward Obligations:
    Implement a system to track the total value of outstanding rewards and ensure that the contract holds enough Ether to cover them. Before distributing rewards, the contract should check whether it has sufficient funds to honor the rewards.

    Example:

function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
uint256 randomValue = getRandomValue() % 100;
// Check available funds before distributing rewards
if (randomValue < 75) {
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95 && prizePoolBalance >= 0.1 ether) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
prizePoolBalance -= 0.1 ether;
} else if (randomValue < 99 && prizePoolBalance >= 0.5 ether) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
prizePoolBalance -= 0.5 ether;
} else if (prizePoolBalance >= 1 ether) {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
prizePoolBalance -= 1 ether;
} else {
revert("Insufficient prize pool funds");
}
boxesOwned[msg.sender] -= 1;
}

By implementing these recommendations, the contract can ensure that it always has sufficient Ether to back the rewards distributed to users, thereby preventing Ether mismanagement and preserving user trust.

Updates

Appeal created

inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
0xgee001 Submitter
8 months ago
inallhonesty Lead Judge
8 months ago
inallhonesty Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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