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 {
emit PlayerLostTheGame("Dealer's hand is higher, dealers winning hand: ", dealerHand);
endGame(msg.sender, false);
}
Proof of Concept:
function testPlayerLosesOnPushButShouldDraw() public {
vm.startPrank(player);
vm.deal(player, 2 ether);
uint256 initialBalance = player.balance;
game.startGame{value: 1 ether}();
vm.store(
address(game),
keccak256(abi.encode(player, uint256(0))),
bytes32(uint256(2))
);
bytes32 playerSlot = keccak256(abi.encode(
keccak256(abi.encode(player, uint256(0)))
));
vm.store(address(game), bytes32(uint256(playerSlot)), bytes32(uint256(12)));
vm.store(address(game), bytes32(uint256(playerSlot) + 1), bytes32(uint256(11)));
vm.store(
address(game),
keccak256(abi.encode(player, uint256(1))),
bytes32(uint256(2))
);
bytes32 dealerSlot = keccak256(abi.encode(
keccak256(abi.encode(player, uint256(1)))
));
vm.store(address(game), bytes32(uint256(dealerSlot)), bytes32(uint256(12)));
vm.store(address(game), bytes32(uint256(dealerSlot) + 1), bytes32(uint256(11)));
assertEq(game.playersHand(player), game.dealersHand(player), "Hands should be equal before call");
game.call();
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:
-
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
-
Game Fairness
Violates fundamental Blackjack rules
Creates an unfair advantage for the house
Breaks player expectations of standard game mechanics
-
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
Recommendations
Implement Proper Push Handling:
function call() public {
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);
} 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);
emit BetReturned(player, 1 ether);
}
}
Add New Event:
event GamePush(string message, uint256 handValue);
event BetReturned(address player, uint256 amount);
Testing Updates:
Add comprehensive tests for push scenarios
Verify correct bet handling in push cases
Test edge cases with different equal hand values