Summary
No cooldowns on RapBattle::goOnStageOrBattle
allow for a DoS
Vulnerability Details
The function RapBattle::goOnStageOrBattle
sets up the enviroment for the NFT RapBattle to happen.
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 {
_battle(_tokenId, _credBet);
}
}
However, the function does not track the last time a NFT battled!
Impact
The attacker can call repeatedly in a loop the function with the same 2 NFTs, betting 0 cred. That would create an infinite loop, creating a DoS attack
Tools Used
Foundry
Proof of Concept:
1: Attacker mints 2 NFTs
2: Attacker calls RapBattle::goOnStageOrBattle
with both NFTs, betting 0 cred, in an infinite loop.
Code
function testDOSAttack() public twoSkilledRappers {
address attacker = makeAddr("Exploiter");
vm.startPrank(attacker);
oneShot.mintRapper();
oneShot.mintRapper();
while (true) {
oneShot.approve(address(rapBattle), 2);
oneShot.approve(address(rapBattle), 3);
rapBattle.goOnStageOrBattle(2, 0);
rapBattle.goOnStageOrBattle(3, 0);
}
vm.stopPrank();
}
Recommendations
Recommended Mitigation:
Adding a mapping that stores the last time an NFT participated in a battle and a cooldown time. Then, checking that last time the function was called is bigger than the cooldown
contract RapBattle {
.
.
.
address public defender;
uint256 public defenderBet;
uint256 public defenderTokenId;
uint256 public constant BASE_SKILL = 65; // The starting base skill of a rapper
uint256 public constant VICE_DECREMENT = 5; // -5 for each vice the rapper has
uint256 public constant VIRTUE_INCREMENT = 10; // +10 for each virtue the rapper has
+ uint256 public constant BATTLE_COOLDOWN = 2 days;
+ mapping(uint256 tokenId => uint256 lastTimeBattle) public battleLog;
.
.
.
function goOnStageOrBattle(uint256 _tokenId, uint256 _credBet) external {
+ require(block.timestamp - battleLog[_tokenId] >= BATTLE_COOLDOWN, "RapBattle: Cooldown still in progress");
+ battleLog[_tokenId] = block.timestamp;
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);
}
}