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

Attacker can select a NFT tokenId that does not own and call `RapBattle::goOnStageOrBattle`

Summary

Attacker can select a NFT tokenId that does not own and call RapBattle::goOnStageOrBattle

Vulnerability Details

The function RapBattle::goOnStageOrBattle sets up the enviroment for the NFT RapBattle to happen. If the one calling is the attacker, it automatically calls RapBattle::_battle , which makes 2 NFTs battle and get a winner from it.

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);
}
}
function _battle(uint256 _tokenId, uint256 _credBet) internal {
address _defender = defender;
require(defenderBet == _credBet, "RapBattle: Bet amounts do not match");
uint256 defenderRapperSkill = getRapperSkill(defenderTokenId);
uint256 challengerRapperSkill = getRapperSkill(_tokenId);
uint256 totalBattleSkill = defenderRapperSkill + challengerRapperSkill;
uint256 totalPrize = defenderBet + _credBet;
uint256 random = uint256(
keccak256(
abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)
)
) % totalBattleSkill;
// Reset the defender
defender = address(0);
emit Battle(
msg.sender,
_tokenId,
random < defenderRapperSkill ? _defender : msg.sender
);
// 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);
}
totalPrize = 0;
// Return the defender's NFT
oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}

However, the function never checks if the _tokenId the attacker uses to call RapBattle::goOnStageOrBattle is linked to a NFT that the user owns.

Impact

The attacker can 'borrow' a NFT from another person that is stronger in order to have better chances to win the battle

Tools Used

Foundry

Proof of Concept:

1: Defender bets 1 Cred and the stats of his Rapper are maxed

2: Attacker own NFT has lower stats than the defender, but he has a friend that has a NFT with maxed stats. So he calls RapBattle::goOnStageOrBattle with the _tokenId of his friend's NFT

3: Then, attacker has a bigger chance of winning the Rap Battle than with his own NFT

Code
function testAttackerCanBorrowNft() public twoSkilledRappers {
address attacker = makeAddr("Exploiter");
//user and challenger have 4 Cred minted because of the `twoSkilledRappers` modifier. user bets 1 Cred
vm.startPrank(user);
oneShot.approve(address(rapBattle), 0);
cred.approve(address(rapBattle), 1);
rapBattle.goOnStageOrBattle(0, 1);
vm.stopPrank();
vm.startPrank(attacker);
oneShot.mintRapper();
oneShot.approve(address(streets), 2);
streets.stake(2);
vm.stopPrank();
vm.warp(block.timestamp + 1 days + 1);
vm.startPrank(attacker);
streets.unstake(2);
oneShot.approve(address(rapBattle), 2);
cred.approve(address(rapBattle), 1);
//We send the Nft of `challenger`(_tokenId = 1) with the account of `attacker` (whose NFT is _tokenId = 2)
rapBattle.goOnStageOrBattle(1, 1);
vm.stopPrank();
//It does not revert!
}

Recommendations

Send the Nft to the contract like the defender:

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);
_battle(_tokenId, _credBet);
}
}
function _battle(uint256 _tokenId, uint256 _credBet) internal {
.
.
.
totalPrize = 0;
+ oneShotNft.transferFrom(address(this), msg.sender, _tokenId);
oneShotNft.transferFrom(address(this), _defender, defenderTokenId);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge almost 2 years 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.

Give us feedback!