Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

[M-2] Missing State Check Allows Winner to Withdraw Prize During an Active Game Round

[M-2] Missing State Check Allows Winner to Withdraw Prize During an Active Game Round

Description

The withdrawWinnings() function lacks an essential state check to ensure it is only callable when the game is in a concluded state. It is missing the gameEndedOnly modifier, which is present on the resetGame() function but was omitted here. This oversight allows the function to be called at any time, provided the caller has a non-zero balance in the pendingWinnings mapping.

// src/Game.sol
// The modifier exists...
modifier gameEndedOnly() {
require(gameEnded, "Game: Game has not ended yet.");
_;
}
// ...but it is missing from the withdrawWinnings function.
function withdrawWinnings() external nonReentrant {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Game: No winnings to withdraw.");
// ...
}

Risk

Likelihood: Medium

The scenario requires a winner from a previous round to not withdraw their prize immediately after the game ends. This is a plausible situation, as a user might forget, be slow to react, or an automated script could fail, leaving them with a pending balance when the next round begins.

Impact: Medium

This vulnerability breaks the intended state machine of the contract, which should clearly separate active and ended game rounds. While it does not allow a random attacker to steal funds, it allows a legitimate (but previous) winner to interfere with an active game. This can lead to race conditions, accounting confusion for off-chain services, and a violation of the game's core lifecycle integrity. It degrades the predictability and robustness of the contract.

Proof of Concept

The following test demonstrates the vulnerability. It simulates a scenario where the winner of Round 1 successfully withdraws their prize during the active phase of Round 2, a state in which withdrawals should be disabled.

function testCanWithdrawWinningsDuringActiveGame() public {
// This test assumes prior bug in claimThrone is fixed
// to demonstrate this specific vulnerability.
// --- Round 1: Player 1 wins ---
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
vm.warp(block.timestamp + GRACE_PERIOD + 1);
game.declareWinner();
assertTrue(game.gameEnded(), "Round 1 should have ended.");
uint256 player1Winnings = game.pendingWinnings(player1);
// --- Game Reset ---
vm.prank(deployer);
game.resetGame();
assertFalse(game.gameEnded(), "Game should be active for Round 2.");
// --- Round 2: Player 2 becomes king ---
vm.prank(player2);
game.claimThrone{value: game.claimFee()}();
// --- The Attack ---
// While Round 2 is active, Player 1 from Round 1 withdraws their prize.
// This call should revert in a fixed contract but succeeds here.
uint256 player1BalanceBefore = player1.balance;
vm.prank(player1);
game.withdrawWinnings();
// Assert that the withdrawal was successful, proving the bug.
assertEq(
player1.balance,
player1BalanceBefore + player1Winnings,
"BUG CONFIRMED: Player 1 withdrew winnings during an active game round."
);
}

Recommended Mitigation

Apply the gameEndedOnly modifier to the withdrawWinnings() function. This will enforce the correct state constraints, ensuring that prizes can only be withdrawn after a game has officially concluded and before a new one has begun.

// src/Game.sol
- function withdrawWinnings() external nonReentrant {
+ function withdrawWinnings() external nonReentrant gameEndedOnly {
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);
}
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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