TwentyOne

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

High probability to get bust on the dealer hand

Summary

The random logic on the dealer hand is high probability to get bust (it is >=50%). In the long run the player will gain all of ETH in the contract.

Vulnerability Details

The dealer hand is depend on the standThreshold (stop drawing a card when the dealer hand value is in the threshold) which is calculate by random number mod by 5 and plus by 17.

TwentyOne.sol

uint256 standThreshold = (uint256(
keccak256(
abi.encodePacked(block.timestamp, msg.sender, block.prevrandao)
)
) % 5) + 17;

From the code above, there are 5 possible value of the standThreshold: {17, 18, 19, 20, 21}.

The dealer need to draw a card to match the value within the randomed threshold.

By our simulation of card drawing using following python code to calculate the bust chance in each threshold:

insecure-blackjack-simulation.py

import random
from collections import Counter
# Constants for card values
CARD_VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] # Aces are 1 for simplicity
DECK_SIZE = 52
# Simulation parameters
NUM_SIMULATIONS = 100000 # Number of simulations per threshold
def simulate_bust_probability(threshold):
"""Simulates the dealer's bust probability for a given threshold."""
bust_count = 0 # Count of busts
total_count = 0 # Total games simulated
for _ in range(NUM_SIMULATIONS):
# Create a fresh deck and dealer's starting hand
deck = CARD_VALUES * 4 # Standard deck (4 suits)
random.shuffle(deck)
dealer_total = 0
busted = False
# Draw cards until dealer reaches or exceeds the threshold
while dealer_total < threshold:
if len(deck) == 0: # No more cards to draw
break
card = deck.pop()
dealer_total += card
# Handle Ace as 11 if it doesn't cause a bust
if card == 1 and dealer_total + 10 <= 21:
dealer_total += 10
# Check for bust condition
if dealer_total > 21:
busted = True
break
total_count += 1
if busted:
bust_count += 1
# Calculate bust probability
return bust_count / total_count
# Simulate for thresholds 17 to 21
thresholds = [17, 18, 19, 20, 21]
bust_probabilities = {t: simulate_bust_probability(t) for t in thresholds}
bust_probabilities

Result:

{17: 0.29727, 18: 0.39748, 19: 0.51045, 20: 0.63884, 21: 0.83136}

The bust probability in each threshold by overall is >=50% by ignoring the player hand.

Threshold Bust Probability
17 29.73%
18 39.75%
19 51.05%
20 63.88%
21 83.14%

This shows the chance that the dealer will lose to the player regardless of the player's hand. When the player's hand is greater than the threshold, the chance that the dealer will lose increases.

Impact

In the long run overall players will win the dealer.

In the worst case, the player may play multiple times, then the dealer loses all the ETH in the contract.

Tools Used

Manual code reading and using python programing to calculate probabilities.

Recommendations

I recommend limiting the bust chance by removing the randomness of the standThreshold and fixed it to 17 to reduce the bust chance for example:

TwentyOne.sol

function call() public {
require(
playersDeck[msg.sender].playersCards.length > 0,
"Game not started"
);
uint256 playerHand = playersHand(msg.sender);
// Dealer draws cards until their hand reaches or exceeds the threshold
while (dealersHand(msg.sender) < 17) {
uint256 newCard = drawCard(msg.sender);
addCardForDealer(msg.sender, newCard);
}
[...]
}
Updates

Lead Judging Commences

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

The Dealer's Play - Dealer must stand on 17

Support

FAQs

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