TwentyOne

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

[H-04] Incorrect Push (Draw) Handling for Blackjack

Summary

The TwentyOne contract incorrectly handles push scenarios (when player and dealer have equal hands) in Blackjack. In standard Blackjack rules, when the player and dealer have equal hands, the player should receive their original bet back. However, the current implementation causes the player to lose their bet in push scenarios, which violates standard game rules and unfairly disadvantages players.

Vulnerability Details

Location: src/TwentyOne.sol

https://github.com/Cyfrin/2024-11-TwentyOne/blob/main/src/TwentyOne.sol#L142-L157

The vulnerability exists in the call() function where the winner is determined. The current implementation treats all non-winning scenarios (including pushes) as losses:

if (dealerHand > 21) {
emit PlayerWonTheGame("Dealer went bust, players winning hand: ", playerHand);
endGame(msg.sender, true);
} else if (playerHand > dealerHand) {
emit PlayerWonTheGame("Dealer's hand is lower, players winning hand: ", playerHand);
endGame(msg.sender, true);
} else { // This else block incorrectly handles pushes as losses
emit PlayerLostTheGame("Dealer's hand is higher, dealers winning hand: ", dealerHand);
endGame(msg.sender, false);
}

Proof of Concept:

function testPlayerLosesOnPushButShouldDraw() public {
// Start game as player
vm.startPrank(player);
vm.deal(player, 2 ether);
uint256 initialBalance = player.balance;
game.startGame{value: 1 ether}();
// Force player's cards to be Queen (12) and Jack (11)
vm.store(
address(game),
keccak256(abi.encode(player, uint256(0))), // slot for playersDeck[player]
bytes32(uint256(2)) // length of array = 2
);
bytes32 playerSlot = keccak256(abi.encode(
keccak256(abi.encode(player, uint256(0))) // slot for playersDeck[player].playersCards array
));
// Store Queen and Jack for player
vm.store(address(game), bytes32(uint256(playerSlot)), bytes32(uint256(12))); // Queen
vm.store(address(game), bytes32(uint256(playerSlot) + 1), bytes32(uint256(11))); // Jack
// Force dealer's cards to also be Queen (12) and Jack (11)
vm.store(
address(game),
keccak256(abi.encode(player, uint256(1))), // slot for dealersDeck[player]
bytes32(uint256(2)) // length of array = 2
);
bytes32 dealerSlot = keccak256(abi.encode(
keccak256(abi.encode(player, uint256(1))) // slot for dealersDeck[player].dealersCards array
));
// Store Queen and Jack for dealer
vm.store(address(game), bytes32(uint256(dealerSlot)), bytes32(uint256(12))); // Queen
vm.store(address(game), bytes32(uint256(dealerSlot) + 1), bytes32(uint256(11))); // Jack
// Verify both hands are equal before calling
assertEq(game.playersHand(player), game.dealersHand(player), "Hands should be equal before call");
// Call to end game
game.call();
// Verify player incorrectly lost their bet on a push
assertEq(
player.balance,
initialBalance - 1 ether,
"Player lost their bet on a push when they should have gotten it back"
);
vm.stopPrank();
}

Impact

The incorrect handling of push scenarios has several significant impacts:

  1. Direct Financial Loss

    • Players lose their entire bet in push scenarios

    • Every push results in unfair loss of player funds

    • Creates an excessive house advantage beyond standard Blackjack rules

  2. Game Fairness

    • Violates fundamental Blackjack rules

    • Creates an unfair advantage for the house

    • Breaks player expectations of standard game mechanics

  3. Trust and Protocol Reputation

    • Players may lose trust in the protocol due to unfair rules

    • Could be perceived as intentionally predatory mechanics

    • May lead to reduced protocol adoption

Tools Used

  • Foundry Testing Framework

  • Manual Code Review

  • Custom test cases for push scenarios

Recommendations

  1. Implement Proper Push Handling:

function call() public {
// ... existing checks ...
uint256 dealerHand = dealersHand(msg.sender);
uint256 playerHand = playersHand(msg.sender);
if (dealerHand > 21) {
emit PlayerWonTheGame("Dealer went bust, players winning hand: ", playerHand);
endGame(msg.sender, true);
} else if (playerHand > dealerHand) {
emit PlayerWonTheGame("Dealer's hand is lower, players winning hand: ", playerHand);
endGame(msg.sender, true);
} else if (playerHand == dealerHand) {
emit GamePush("Push - equal hands", playerHand);
endGame(msg.sender, false, true); // New parameter for push should send 1 eth back to player
} else {
emit PlayerLostTheGame("Dealer's hand is higher, dealers winning hand: ", dealerHand);
endGame(msg.sender, false);
}
}
function endGame(address player, bool playerWon, bool isPush) 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 if (isPush) {
payable(player).transfer(1 ether); // Return original bet
emit BetReturned(player, 1 ether);
}
}
  1. Add New Event:

event GamePush(string message, uint256 handValue);
event BetReturned(address player, uint256 amount);
  1. Testing Updates:

    • Add comprehensive tests for push scenarios

    • Verify correct bet handling in push cases

    • Test edge cases with different equal hand values

Updates

Lead Judging Commences

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

Tie case

Support

FAQs

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