TwentyOne

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

Incorrect Winner Determination Logic: Player Receives Payout Even on Loss

Summary

The call() function in the TwentyOne contract contains a critical logic flaw in how the winner of the game is determined. Specifically, the contract fails to correctly evaluate the conditions under which the dealer wins. This results in players receiving a payout even when their hand is lower than the dealer’s, effectively guaranteeing them winnings regardless of the game's outcome.

Vulnerability Details

Expected Behavior:
The player's balance should remain the same after losing the game.
Observed Behavior:
The player's balance increases, indicating a payout despite the dealer winning.
Root Cause Analysis:
The issue lies in the call() function logic for determining the winner. The current implementation does not correctly account for the following conditions:
Player loses:
The dealer’s hand is greater than the player’s hand, and neither hand exceeds 21.
Dealer bust:
The dealer’s hand exceeds 21, and the player wins.
Tie:
Handle cases where both hands are equal appropriately.
For the test to work there's a need for helper functions that need to be added to the `TwentyOne.sol`
Add the following to the `TwentyOne.s.sol` file.
<details>
<summary>Code</summary>
```javascript
function testDrawCard(address player) public returns (uint256) {
return drawCard(player);
}
function testAddCardForDealer(address player, uint256 card) public {
addCardForDealer(player, card);
}```
</details>
The following test demonstrates the issue:
Add the following to the `TwentyOne.t.sol` test file.
<details>
<summary>Code</summary>
```javascript
function test_Call_PlayerLoses_Debug() public {
vm.startPrank(player1); // Start acting as player1
// Start the game with 1 ether bet
twentyOne.startGame{value: 1 ether}();
uint256 playerHand = twentyOne.playersHand(player1);
uint256 dealerHand;
// Simulate the dealer's hand behavior
while (dealerHand < 19) {
// Force dealer to have a higher hand
uint256 card = twentyOne.testDrawCard(player1); // Use test helper
twentyOne.testAddCardForDealer(player1, card); // Use test helper
dealerHand = twentyOne.dealersHand(player1);
}
console.log("Player Hand:", playerHand);
console.log("Dealer Hand:", dealerHand);
uint256 initialBalance = player1.balance;
// Call to end the game
twentyOne.call();
uint256 finalBalance = player1.balance;
// Check outcome
if (dealerHand > playerHand && dealerHand <= 21) {
// Player should lose
// assertEq(finalBalance, initialBalance, "Player balance should remain unchanged on loss"); -->
} else if (playerHand > dealerHand || dealerHand > 21) {
// Player should win
assertEq(finalBalance, initialBalance + 2 ether, "Player balance should increase by 2 ETH on win");
} else {
revert("Unexpected game outcome");
}
console.log("player final balance: ", finalBalance);
vm.stopPrank();
}```
</details>

Impact

Incorrect Payouts:
Players receive winnings even when they lose, leading to financial loss for the contract owner and undermining the game's integrity.
Exploitation Risk:
Players or malicious actors can deliberately lose the game and still receive a payout, draining the contract's funds and rendering the game unsustainable.
Loss of Trust:
Honest players who notice the flaw may lose confidence in the fairness of the game, negatively impacting its reputation.
Contract Insolvency:
Continuous payouts regardless of game outcomes could quickly deplete the contract's balance, making it unable to pay legitimate winners.

Tools Used

FOUNDRY

Recommendations

1. **Update the call() Function Logic:**
Ensure the correct conditions for payouts are implemented. For example:
_<details>
<summary>Code</summary>
```diff
// Determine the winner
- 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);
- }
+ if (playerHand > 21) {
+ revert("Player went bust");
+ }
+ if (dealerHand > 21 || (playerHand <= 21 && playerHand > dealerHand)) {
+ // Player wins
+ emit PlayerWonTheGame("Player wins with hand: ", playerHand);
+ payable(msg.sender).transfer(address(this).balance);
+ } else {
+ // Dealer wins
+ emit GameResult("Dealer wins with hand: ", dealerHand);
+ }```
</details>
1. **Comprehensive Testing:**
Add test cases for all possible scenarios:
Dealer wins with a higher hand.
Player wins with a higher hand.
Both dealer and player exceed 21 (bust).
A tie occurs.
3. **Validate Before Payouts:**
Before transferring funds, validate the winner using precise game logic and ensure no payout occurs on a loss.
3. **Event Logging:**
Emit detailed events in all scenarios to help track the game flow and debug issues if needed.
_<details>
<summary>Code</summary>
```diff
+ event GameResult(string message, uint256 cardsTotal);
```
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Appeal created

osuolale Submitter
11 months ago
inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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