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

`RapBattle::_battle()` can emit incorrect event parameters

Summary

The RapBattle::_battle() function can emit incorrect event parameters and make it appear that the challenger won the battle when in actuality the defender has won.

Vulnerability Details

The RapBattle::_battle() function emits the Battle event with 3 parameters, the challenger (msg.sender), the _tokenId, and the winner of the battle:

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);
}

The winner of the battle is determined by the condition that if random is less than the defenderRapperSkill then the event emits with the _defender as the winner. Where as if random is greater than defenderRapperSkill, then the event emits with the msg.sender (the challenger) as the winner.

This statement contradicts the following condition after the event is emitted that determines who wins the funds. As that statement determines the winner on the condition that if random is less than or equal to the defenderRapperSkill then the _defender wins the tokens.

Due to the mismatch of conditions, there is a scenario where if the random number generated is equal to the defenderRapperSkill, the event emitted will emit with msg.sender (the challenger) as the winner but the funds will get transferred to the _defender.

Impact

  • The Battle event can emit the wrong winner of the battle

POC

Below you can see a POC that shows how this can happen:

function testIncorrectBattleEvent() public twoSkilledRappers {
uint256 DEFENDER_AMOUNT_TO_BET = cred.balanceOf(user);
uint256 CHALLENGER_AMOUNT_TO_BET = cred.balanceOf(challenger);
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), DEFENDER_AMOUNT_TO_BET);
rapBattle.goOnStageOrBattle(0, DEFENDER_AMOUNT_TO_BET);
vm.stopPrank();
vm.startPrank(challenger);
vm.warp(54 seconds);
oneShot.approve(address(rapBattle), 1);
cred.approve(address(rapBattle), CHALLENGER_AMOUNT_TO_BET);
vm.recordLogs();
rapBattle.goOnStageOrBattle(1, CHALLENGER_AMOUNT_TO_BET);
vm.stopPrank();
Vm.Log[] memory entries = vm.getRecordedLogs();
address winner = address(uint160(uint256(entries[0].topics[2])));
assert(winner == challenger); // The winner emitted from the event is the challenger...
assert(cred.balanceOf(challenger) == 0); // However the challenger actually lost...
assert(
cred.balanceOf(user) ==
DEFENDER_AMOUNT_TO_BET + CHALLENGER_AMOUNT_TO_BET
);
}

Tools Used

VS Code, Foundry

Recommendations

Update the condition of the determined winner when emitting the event:

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);
+ 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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Contradictory battle result event

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.