Mystery Box

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

Pseudo-Randomness Issue in openBox() function

Summary

The randomness mechanism in openBox() is predictable and can be manipulated, allowing attackers to influence the outcome of rewards.

Vulnerability Details (POC)

Affected Code:

uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;

Using block.timestamp and msg.sender for randomness is insecure because:

  • block.timestamp: Can be influenced within a range by miders/validators.

  • msg.sender: Known to the attacker.

Scenario

An attacker aims to win the "Gold Coin" reward (1% chance):

  1. Preparation:

    • Deploys a contract to repeatedly call openBox() until they receive the desired reward.

  2. Manipulation:

    • Adjusts the timing of transactions to affect block.timestamp.

    • Uses multiple addresses (controlled accounts) to influence msg.sender.

  3. Execution:

    • Calls openBox() in rapid succession.

    • Monitors the randomValue outcome (if possible).

  4. Result:

    • Increases chances of hitting randomValue == 99.

    • Successfully obtains the "Gold Coin".

Reference to Test Code

This vulnerability is not directly demonstrated in the test code but can be emphasized by adding a test that shows the predictability:

function testPredictableRandomness() public {
vm.prank(user1);
mysteryBox.buyBox{value: 0.1 ether}();
// Manipulate block timestamp
vm.warp(block.timestamp + 1);
vm.prank(user1);
mysteryBox.openBox(); // Check the reward received
}

By manipulating block.timestamp, we can influence randomValue.

Impact

  • Unfair Advantage: Attackers can manipulate outcomes to win valuable rewards.

  • Financial Loss: The contract may pay out more than intended.

  • Trust Issues: Users may distrust the fairness of the reward system.

Tools Used

  • Manual Code Analysis: Identified the insecure randomness source.

  • Testing with Manipulated Conditions: Used vm.warp to alter block.timestamp.

Recommendations

Implement secure randomness using Chainlink VRF:

note: this is an exmaple implementation of how this can be solved using chainlink VRF for secure randomness

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MysteryBox is VRFConsumerBase, Ownable(msg.sender) {
bytes32 internal keyHash;
uint256 internal fee;
mapping(bytes32 => address) private requestToUser;
mapping(address => string[]) public rewardsOwned;
mapping(address => uint256) public boxesOwned;
constructor(
address _vrfCoordinator,
address _linkToken,
bytes32 _keyHash,
uint256 _fee
) VRFConsumerBase(_vrfCoordinator, _linkToken) {
keyHash = _keyHash;
fee = _fee;
}
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
bytes32 requestId = requestRandomness(keyHash, fee);
requestToUser[requestId] = msg.sender;
boxesOwned[msg.sender] -= 1;
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
uint256 randomValue = randomness % 100;
address user = requestToUser[requestId];
if (randomValue < 10) {
rewardsOwned[user].push("Legendary Item");
} else if (randomValue < 40) {
rewardsOwned[user].push("Rare Item");
} else {
rewardsOwned[user].push("Common Item");
}
}
function fundContract(uint256 amount) public onlyOwner {
LINK.transferFrom(msg.sender, address(this), amount);
}
}
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!