Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Weak Randomess in `RapBattle::_battle` Allows Users to Influence / Predict Battle Winners

Vulnerability Details

RapBattle::_battle hashes msg.sender, block.timestamp, and block.prevrandao to create a supposedly random number that is eventually used to determine the winner of a rap battle. However, hashing these values does not create a truly random number. Malicious users can manipulate these values or know them ahead of time to choose the winner of the battle.

  • Validators can know ahead of time the block.timestamp.

  • prevrandao suffers from biasibility, miners can know its value ahead of time

  • User can mine/manipulate their msg.sender value to result in their address being used to generate the winner.

Blockchains are deterministic systems by nature, designed to achieve consensus across all nodes. Using on-chain values as a randomness seed is a well-documented attack vector in the blockchain space.

Impact

Users can influence / predict the winner of battles.

Proof of Code

Insert the following piece of code to OneShotTest.t.sol:

function test_weakRandomness() public {
address winner;
address previousWinner;
address defender = makeAddr("defender");
vm.prank(defender);
oneShot.mintRapper();
assertEq(oneShot.ownerOf(0), defender);
vm.prank(challenger);
oneShot.mintRapper();
assertEq(oneShot.ownerOf(1), challenger);
// cheking that the rapper skills are the same, ensuring a theoretical chance of 50% for each party to win a battle
assertEq(rapBattle.getRapperSkill(0), rapBattle.getRapperSkill(1));
console.log("Block timestamp: ", block.timestamp);
console.log(""); // empty log line
console.log("Defender: ", defender);
console.log("Challenger: ", challenger);
console.log(""); // empty log line
for (uint256 i = 0; i < 100; i++) {
previousWinner = winner;
vm.startPrank(defender);
// approval is cleared each time the NFT moved, so we do need this in the for loop
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, 0);
vm.stopPrank();
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
vm.recordLogs();
rapBattle.goOnStageOrBattle(1, 0);
vm.stopPrank();
Vm.Log[] memory entries = vm.getRecordedLogs();
winner = address(uint160(uint256(entries[0].topics[2])));
if (i > 0) {
assertEq(winner, previousWinner);
}
}
console.log("Winner: ", winner);
}

and run it by executing forge test --mt test_weakRandomness -vvv.

This gives the following output:

Running 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] test_weakRandomness() (gas: 10371401)
Logs:
Block timestamp: 1
Defender: 0x4fAD2c378d74800f0Be9eAbE53e1236dE790806F
Challenger: 0x2c0169543641108F2d2c34489DBDab1A54cF59b2
Winner: 0x2c0169543641108F2d2c34489DBDab1A54cF59b2
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 33.72ms

According to the test, when the same challenger and defender battle for 100 times, the challenger wins in all 100 cases. Since the RapperSkill of the challenger's and the defender's NFTs are the same, each party should have a 50% chance of winning a battle. Winning 100 battles after each other has as less chance as 7.8886091e-29 %, so by random chance we certainly cannot expect one party to win all 100 battles, indicating weakness in the PRNG used in the protocol.

(Note: The testing environment provided by Foundry's Anvil is static, and block properties are not advanced except if specifically manipulated. If the randomness generation mechanism were truly random, we would expect to see variation in outcomes even in a controlled environment like Anvil. A truly random process should yield different outcomes under identical conditions because it does not depend solely on deterministic inputs.)

Tools Used

Manual review, Foundry.

Recommendations

Consider using a cryptographically provable random number generator, such as Chainlink VRF.

Updates

Lead Judging Commences

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