Description: The RapBattle::_battle
function does not include logic to handle the scenario where the randomly generated random
value is equal to defenderRapperSkill
. This means that when random
equals defenderRapperSkill
, indicating a tie or draw in the battle, the contract's behavior is undefined.
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);
}
Detail: As seen in the highlighted part of the function, there is no explicit check if the random value is equal to the defenderRapperSkill, instead there is a check for less or equal to but even if there is a draw the defender still gets rewarded both bets which is quite unfair.
Impact: The lack of handling for equal skill could lead to confusion or undesired outcomes for users such as the bearing the reward of both bets even though there was a draw.
Proof of Concept:
When the _battle
function is called with the random
equal to defenderRapperSkill
, If both let's say are equal to 50
.
The function's logic does not handle the scenario, leading to an unfair outcome whereby the defender
address gets to win the total of both bets in a draw situation.
Recommended Mitigation: Introduce logic to handle the case where random
is equal to defenderRapperSkill
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);
- }
// Handle Equal Skill
+ if (random == defenderRapperSkill) {
// Refund the bets to both players
+ credToken.transfer(msg.sender, _credBet);
+ credToken.transfer(_defender, defenderBet);
+ } else if (random < defenderRapperSkill) {
// Defender wins
+ credToken.transfer(_defender, defenderBet);
+ credToken.transferFrom(msg.sender, _defender, _credBet);
+ } else {
// Challenger wins
+ credToken.transfer(msg.sender, _credBet);
+ }
totalPrize = 0;
oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}
Explanation:
Added a check for random == defenderRapperSkill
to handle the tie scenario.
If random
equals defenderRapperSkill
, both the defender
and the challenger
will receive a refund of their bets. This ensures that the contract handles all possible outcomes of the battle, including ties, in a fair and defined manner.
Final Note: Implementing this mitigation ensures that the contract behavior is consistent and fair, preventing unexpected outcomes and providing a clear resolution for tie scenarios in the battle.