Summary
The contract logic does not correctly handle scenarios where both the Dealer's hand and the Player's hand exceed 21 simultaneously. In the current implementation, the Player automatically wins if the Dealer's hand exceeds 21, regardless of the Player's own hand total.
Vulnerability Details
When both the Player's hand and the Dealer's hand exceed 21, the contract does not check the Player's hand before determining the winner. Instead, it assumes the Player wins as long as the Dealer's hand exceeds 21. This oversight leads to situations where the Player can exploit this logic to win games they should lose.
PoC
Please include the following in the TwentyOne.sol contract.
// add "mapping(address => bool) private gameResults;"
mapping(address => PlayersCards) playersDeck;
mapping(address => DealersCards) dealersDeck;
+ mapping(address => bool) private gameResults; // just added.
function endGame(address player, bool playerWon) internal {
delete playersDeck[player].playersCards; // Clear the player's cards
delete dealersDeck[player].dealersCards; // Clear the dealer's cards
delete availableCards[player]; // Reset the deck
if (playerWon) {
payable(player).transfer(2 ether);
+ gameResults[player] = playerWon;
emit FeeWithdrawn(player, 2 ether); // Emit the prize withdrawal event.
}
}
+ function getGameResult(address player) public view returns (bool) {
+ return gameResults[player];
+ }
Add this function to the test file.
function testCallwhenBoth22() public {
vm.deal(address(twentyOne), 20 ether);
vm.startPrank(player1);
twentyOne.startGame{value: 1 ether}();
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand(address)", player1),
abi.encode(22)
);
console.log("here is the dealer's hand", twentyOne.dealersHand(player1));
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("playersHand(address)", player1),
abi.encode(22)
);
console.log("here is the player's hand", twentyOne.playersHand(player1));
twentyOne.call();
bool gameResult = twentyOne.getGameResult(player1);
assertEq(gameResult, true, "Expected player to lose the game");
vm.stopPrank();
}
Result:
Ran 1 test for test/TwentyOne.t.sol:TwentyOneTest
[PASS] testCallwhenBoth22() (gas: 1176837)
Logs:
here is the dealer's hand 22
here is the player's hand 22
playerWon is equal: true
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.09ms (21.41ms CPU time)
```
Impact
Incorrect Winner Determination: Players win games they should lose or draw, undermining the contract's integrity.
Tools Used
Recommendations
Modify the call function to explicitly check both the Dealer's and the Player's hands before deciding the outcome. i.e.
if (playerHand > 21 && dealerHand > 21)