Lack of Game-End Check in withdrawWinnings()
Allows Premature Withdrawals
Description
-
Players should only be allowed to withdraw winnings after the game has concluded and the winner has been officially declared via declareWinner()
.
-
The withdrawWinnings()
function allows players to withdraw any available pendingWinnings
even before the game has ended, since it doesn't check whether gameEnded
is true
. This breaks the game flow — players could withdraw rewards that haven't been fully finalized, especially if the contract logic is later changed to assign rewards mid-game (e.g., on claimThrone()
).
function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
@> require(amount > 0, "Game: No winnings to withdraw.");
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Game: Failed to withdraw winnings.");
pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}
Risk
Likelihood:
-
A player receives non-zero pendingWinnings
during an early phase (e.g., due to future bugs or reward changes).
-
They immediately withdraw it before the game ends, bypassing intended restrictions.
-
No protections stop this today.
Impact:
-
Game reward logic becomes inconsistent.
-
In future extensions (like partial reward pre-allocation), this opens up premature extraction and economic exploits.
-
Reduces trust and fairness of the game.
Proof of Concept
gameEnded = false;
pendingWinnings[attacker] = 5 ether;
function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Game: Failed to withdraw winnings.");
pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}
Recommended Mitigation
function withdrawWinnings() external nonReentrant {
+ require(gameEnded, "Game: Cannot withdraw before the game ends.");
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "Game: Failed to withdraw winnings.");
pendingWinnings[msg.sender] = 0;
emit WinningsWithdrawn(msg.sender, amount);
}
Or, if the contract might allow **some early payouts** in the future:
require(gameEnded || earlyWithdrawAllowed[msg.sender], "Game: Not eligible to withdraw yet.");