TwentyOne

First Flight #29
Beginner FriendlyGameFiFoundrySolidity
100 EXP
View results
Submission Details
Severity: high
Invalid

`Dealer` is griefed with no rewards if `player` goes bust.

Description:

https://github.com/Cyfrin/2024-11-TwentyOne/blob/a4429168302722d14a5e5996d25d6fc5be22a899/src/TwentyOne.sol#L104
https://github.com/Cyfrin/2024-11-TwentyOne/blob/a4429168302722d14a5e5996d25d6fc5be22a899/src/TwentyOne.sol#L165

The current implementation of the endgame function introduces a vulnerability where the dealer is griefed if the player surpasses the hand value threshold of 21 by calling the hit function. Specifically:

If the player’s total hand value exceeds 21, the game logic marks the player as having lost (playerWon set to false).

However, no mechanism rewards the dealer in this scenario, leaving the dealer with no compensation even when the player busts. This behavior is outlined in the endgame function:

  • L104: The playerWon condition determines if the player's balance is increased.

  • L165: No reward transfer occurs to the dealer if the player loses, even though they have forfeited their bet.

This creates an imbalance, as the dealer receives no benefit when the player busts, effectively rendering the dealer's participation in the game disadvantageous.

Impact:

The absence of a reward for the dealer when the player busts leads to the following critical issues:

a. Economic Imbalance:

The dealer bears the risk of the game but is not rewarded when the player loses due to exceeding 21.
This could discourage dealers from participating in the game, undermining its viability.

b. Griefing Opportunity:

A malicious player can intentionally "bust" (surpass 21) repeatedly to grief the dealer, causing operational and financial strain without incurring meaningful consequences.

c. Loss of Incentive:

Dealers lose the primary incentive to participate in games, as the current logic fails to fairly distribute rewards when players make errors.

Proof of Concept:

Place this in the TwentyOne.sol test suite.

function test_IfDealerisRewardedWhenPlayerGoesBust() public {
// act as player1
vm.startPrank(player1);
// start the game with 1 ether bet.
twentyOne.startGame{value: 1 ether}();
// check the player has two cards.
uint256[] memory playerCards = twentyOne.getPlayerCards(player1);
assertEq(playerCards.length, 2);
// now get the sum of the two cards.
(uint256 sum) = twentyOne.playersHand(player1);
console.log("This is the sum of player1's cards", sum);
// Here lets say that player1 wants to be drawn another card. This time he unknowingly sets it above 21.
twentyOne.hit();
twentyOne.hit();
(uint256 sum1) = twentyOne.playersHand(player1);
// here the cards have been reset.
console.log(sum1);
// test the balances.
uint256 balance = player1.balance;
assert(balance == 9 ether);
}

This are the logs that were outputted::

Traces:
[719223] TwentyOneTest::setUp()
├─ [673708] → new TwentyOne@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 3365 bytes of code
├─ [0] VM::deal(0x0000000000000000000000000000000000000123, 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::deal(0x0000000000000000000000000000000000000456, 10000000000000000000 [1e19])
│ └─ ← [Return]
└─ ← [Stop]
[1374329] TwentyOneTest::test_IfDealerisRewardedWhenPlayerGoesBust()
├─ [0] VM::startPrank(0x0000000000000000000000000000000000000123)
│ └─ ← [Return]
├─ [1270369] TwentyOne::startGame{value: 1000000000000000000}()
│ └─ ← [Return] 14
├─ [1350] TwentyOne::getPlayerCards(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] [23, 43]
├─ [0] VM::assertEq(2, 2) [staticcall]
│ └─ ← [Return]
├─ [2234] TwentyOne::playersHand(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] 14
├─ [0] console::log("This is the sum of player1's cards", 14) [staticcall]
│ └─ ← [Stop]
├─ [30175] TwentyOne::hit()
│ └─ ← [Stop]
├─ [44892] TwentyOne::hit()
│ ├─ emit PlayerLostTheGame(message: "Player is bust", cardsTotal: 27)
│ └─ ← [Stop]
├─ [584] TwentyOne::playersHand(0x0000000000000000000000000000000000000123) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log(0) [staticcall]
│ └─ ← [Stop]
└─ ← [Stop]

From the above logs. Once the endgame function has been called when the player went bust. The funds that the player used to start the game is locked entirely in the contract since there is no handling of rewards if the player went bust.

Tools Used:

Manual Review and Foundry.

Recommended Mitigation:

Add an else block in the endgame function to facilitate the rewarding of the dealer if the player goes bust.

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);
emit FeeWithdrawn(player, 2 ether);
+ } else {
+ // Player loses, dealer wins the bet
+ payable(dealer).transfer(totalBet);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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