TwentyOne

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

[H-03] Missing Balance Checks Allow Contract to be Drained An Unable to Payout Player Wins

Summary

The TwentyOne contract fails to implement proper balance checks before processing payouts. Since each win pays out 2 ETH but each game only requires 1 ETH to play, the contract will eventually become insolvent and unable to pay winners, regardless of whether the wins are concurrent or sequential.

Vulnerability Details

Location: src/TwentyOne.sol

The vulnerability exists in the fundamental economics of the game:

  1. Players bet 1 ETH to play

  2. Winners receive 2 ETH payout

  3. No balance checks before payouts

  4. No house funding mechanism

function startGame() public payable returns (uint256) {
address player = msg.sender;
require(msg.value >= 1 ether, "not enough ether sent"); // Only requires 1 ETH to play
// ... rest of function
}
function endGame(address player, bool playerWon) internal {
delete playersDeck[player].playersCards;
delete dealersDeck[player].dealersCards;
delete availableCards[player];
if (playerWon) {
payable(player).transfer(2 ether); // Pays out 2 ETH on win
emit FeeWithdrawn(player, 2 ether);
}
}

Proof of Concept:

function testContractDrainThroughWins() public {
// Start with 3 ETH in contract
vm.deal(address(game), 3 ether);
// Player has enough ETH for 3 games
vm.deal(player1, 3 ether);
// First game - win
vm.startPrank(player1);
game.startGame{value: 1 ether}();
forceWinningHand(player1);
game.call(); // Wins 2 ETH, contract now has 2 ETH
// Second game - win
game.startGame{value: 1 ether}();
forceWinningHand(player1);
game.call(); // Wins 2 ETH, contract now has 1 ETH
// Third game - win attempt will fail
game.startGame{value: 1 ether}();
forceWinningHand(player1);
vm.expectRevert();
game.call(); // Will revert as contract only has 2 ETH but needs to pay out 2 ETH
vm.stopPrank();
}

Impact

The vulnerability has several severe implications:

  1. Guaranteed Insolvency

    • Each winning player reduces contract balance by 1 ETH (receives 2 ETH, paid 1 ETH)

    • Contract will eventually run out of funds with enough wins

    • No mechanism to replenish contract funds

  2. Game Fairness

    • Winners may not receive their legitimate winnings

    • Players could lose their bets without any chance of receiving winnings

    • Game becomes unplayable once contract is drained

  3. Economic Design Flaw

    • House edge is negative (-100%) as players bet 1 ETH to win 2 ETH

    • No sustainable economic model for contract operation

    • Contract requires constant external funding to remain solvent

Tools Used

  • Foundry Testing Framework

  • Manual Code Review

  • Custom test cases for concurrent winning scenarios

Recommendations

  1. Implement Balance Checks:

function endGame(address player, bool playerWon) internal {
delete playersDeck[player].playersCards;
delete dealersDeck[player].dealersCards;
delete availableCards[player];
if (playerWon) {
require(
address(this).balance >= 2 ether,
"Insufficient contract balance for payout"
);
payable(player).transfer(2 ether);
emit FeeWithdrawn(player, 2 ether);
}
}
  1. Implement Game State Management:

    • Track total potential payouts based on active games

    • Prevent new games from starting if insufficient funds for potential payouts

    • Consider implementing a reserve requirement

  2. Add Circuit Breaker:

modifier sufficientPayout() {
require(
address(this).balance >= 2 ether,
"Insufficient balance for potential payout"
);
_;
}
function startGame() public payable sufficientPayout returns (uint256) {
// ... existing code ...
}
  1. Consider Alternative Payout Models:

    • Implement a queue-based payout system

    • Add partial payout capabilities

    • Consider implementing a maximum concurrent player limit

  2. Improve Monitoring:

    • Add events for failed payouts

    • Track and monitor contract balance

    • Implement automatic top-up mechanisms when balance is low

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

Contract Lacks Mechanism to Initialize or Deposit Ether

Support

FAQs

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