TwentyOne

First Flight #29
Beginner FriendlyGameFiFoundrySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

Denial of Service Due to Insufficient Contract Balance.

Summary

The game contract can enter a state where it becomes unplayable due to insufficient funds in its balance. This results in the inability to process payouts during the endGame function, leading to a denial of service (DoS) for future games.

Vulnerability Details

The endGame function relies on the contract’s balance to pay out winnings. If the balance drops below the required payout amount, subsequent games cannot be completed. This leads to a DoS scenario, effectively halting the game for all players. Such a situation can arise if multiple winning payouts deplete the contract’s funds.

PoC
Add this test to the TwentyOne.t.sol file.

function testDos_DueDrainedFunds() public{
deal(address(twentyOne), 2 ether);
uint256 initialBalance = address(twentyOne).balance;
// uint256 gamebalance = twentyOne.balance();
console.log("this is the balance of twentyOne before game:", initialBalance);
vm.startPrank(player1);
// now let's simulate a situation where player1 plays wice and win in each cases.
twentyOne.startGame{value: 1 ether}();
console.log("Balance after player1 initializes first game:", address(twentyOne).balance);
// Mock the dealer's hand to ensure Player1 wins
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand()"),
abi.encode(21) // Dealer's hand total is fixed at 18
);
// If Player1's hand isn't already winning, draw cards to ensure it's > 18 but <= 21
uint256 player1Hand = twentyOne.playersHand(player1);
while (player1Hand <= 10) {
twentyOne.hit();
console.log("Player1's hand total after hitting:", player1Hand);
}
// Player1 calls to compare hands and win
twentyOne.call();
console.log("this is the balance of twentyOne after player1 firstgame", address(twentyOne).balance);
twentyOne.startGame{value: 1 ether}();
console.log("this is the balance of twentyOne after player1 secondgame was intialized", address(twentyOne).balance);
// win two simulation and endgame.
// Mock the dealer's hand to ensure Player1 wins
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand()"),
abi.encode(21) // Dealer's hand total is fixed at 18
);
// If Player1's hand isn't already winning, draw cards to ensure it's > 18 but <= 21
while (player1Hand <= 10) {
twentyOne.hit();
console.log("Player1's hand total after hitting:", player1Hand);
}
// Player1 calls to compare hands and win
twentyOne.call();
require(address(twentyOne).balance < 1 ether,"Test failed: Contract should not have enough balance to process payouts.");
console.log("this is the balance of twentyOne after player1 secondgame", address(twentyOne).balance);
vm.stopPrank();
vm.startPrank(player2);
twentyOne.startGame{value: 1 ether}();
console.log("this is the balance of twentyOne after player2 firstgame was intialized", address(twentyOne).balance);
// Mock the dealer's hand to ensure Player1 wins
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand()"),
abi.encode(21) // Dealer's hand total is fixed at 18
);
// If Player1's hand isn't already winning, draw cards to ensure it's > 18 but <= 21
uint256 player2Hand = twentyOne.playersHand(player2);
while (player2Hand <= 10) {
twentyOne.hit();
console.log("Player1's hand total after hitting:", player2Hand);
}
// Player1 calls to compare hands and win
twentyOne.call();
require(address(twentyOne).balance == 1 ether,"Test failed: Contract should not have enough balance to process payouts.");
console.log("this is the balance of twentyOne after", address(twentyOne).balance);
vm.stopPrank();
}

Run forge test --match-test testDos_DueDrainedFunds -vvv on your terminal.

Result:

Ran 1 test for test/TwentyOne.t.sol:TwentyOneTest
[PASS] testDos_DueDrainedFunds() (gas: 3404940)
Logs:
this is the balance of twentyOne before game: 2000000000000000000
Balance after player1 initializes first game: 3000000000000000000
this is the balance of twentyOne after player1 firstgame 1000000000000000000
this is the balance of twentyOne after player1 secondgame was intialized 2000000000000000000
this is the balance of twentyOne after player1 secondgame 0
this is the balance of twentyOne after player2 firstgame was intialized 1000000000000000000
this is the balance of twentyOne after 1000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.65ms (2.61ms CPU time)
Ran 1 test suite in 40.67ms (3.65ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

  • Since the transfer is handled internally by the endGame function the player might not be payed.

  • This inconsistency in staye may affect other future games, in the case of several multiple wins.

Tools Used

Foundry 0.2.0 and the console functionality.

Recommendations

  • Preemptive Balance Checks:
    Add a check in the startGame function to ensure the contract has enough funds to process the largest possible payout.

require(
address(this).balance >= 1 ether,
"Contract balance too low to start a new game."
);
  • Dynamic Game Payout Limits:

Introduce a dynamic payout cap based on the contract’s current balance, ensuring that payouts are always possible without draining the contract.

Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Insufficient balance for payouts / Lack of Contract Balance Check Before Starting Game

Support

FAQs

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