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);
twentyOne.startGame{value: 1 ether}();
uint256 playerHand = twentyOne.playersHand(player1);
uint256 dealerHand;
while (dealerHand < 19) {
uint256 card = twentyOne.testDrawCard(player1);
twentyOne.testAddCardForDealer(player1, card);
dealerHand = twentyOne.dealersHand(player1);
}
console.log("Player Hand:", playerHand);
console.log("Dealer Hand:", dealerHand);
uint256 initialBalance = player1.balance;
twentyOne.call();
uint256 finalBalance = player1.balance;
if (dealerHand > playerHand && dealerHand <= 21) {
} else if (playerHand > dealerHand || dealerHand > 21) {
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
- 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)) {
+
+ emit PlayerWonTheGame("Player wins with hand: ", playerHand);
+ payable(msg.sender).transfer(address(this).balance);
+ } else {
+
+ 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);
```