Summary
TwentyOne
might not be capable for paying to winners
Vulnerability Details
Payouts are collected from players, but they are only sufficient to cover half of the active players in the event that all of them win. Additionally, users can start a new game even when there are not enough funds to cover payouts in the case of wins.
Even when the protocol initiates with funds, the risk is possible.
Impact
The game is unfair with the winners, who might not receive their payout if they win.
Tools Used
-
Start a Game =>
function test_NotEnoughToPayout() public {
vm.startPrank(player1);
twentyOne.startGame{ value: 1 ether }();
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand(address)", player1),
abi.encode(18)
);
vm.expectRevert();
twentyOne.call();
vm.stopPrank();
}
Uncovered Payouts
function _makeWin(address player) internal {
vm.startPrank(player);
vm.mockCall(
address(twentyOne),
abi.encodeWithSignature("dealersHand(address)", player),
abi.encode(18)
);
twentyOne.call();
vm.stopPrank();
}
function test_NotEnoughToPayForAll() public {
address player3 = makeAddr("player3");
address player4 = makeAddr("player4");
vm.deal(player3, 1 ether);
vm.deal(player4, 1 ether);
vm.prank(player1);
twentyOne.startGame{ value: 1 ether }();
vm.prank(player2);
twentyOne.startGame{ value: 1 ether }();
vm.prank(player3);
twentyOne.startGame{ value: 1 ether }();
vm.prank(player4);
twentyOne.startGame{ value: 1 ether }();
_makeWin(player1);
_makeWin(player3);
vm.expectRevert();
_makeWin(player4);
}
Recommendations
Start a new game if the worst case is covered (all players win).
Initiate the Game with at least 2 ether.
Monitor its balance otherwise game unplayable.
//tracka number of active players
+ uint256 activePlayers;
//@notice it starts the game only if the worst case is covered
//@dev increase the number of active players
function startGame() public payable returns (uint256) {
+ require(_canPayout(),"Could not be payout")
//impl
+ ++activePlayers;
}
//@notice decrease the number of active players
function endGame(address player, bool playerWon) internal {
//impl
+ --activePlayers;
}
/**
* @notice determines whether the joined player can be paid out
*/
+ function _canPayout() public view returns (bool) {
+ return address(this).balance >= (activePlayers + 1) * 2;
+ }
this way players will be sure on-chain that in case they win the game will be fair.