Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

GameEnded Event Emits Incorrect Zero Value for Prize Amount

GameEnded Event Emits Incorrect Zero Value for Prize Amount

The root cause is a logical error in the ordering of operations. The state variable pot is reset to zero before it is used in the emit statement, leading to the emission of stale, incorrect data.

Description

  • The declareWinner() function emits the GameEnded event with incorrect data. Specifically, it logs the prizeAmount as 0 in every game round. This occurs because the function first transfers the winnings from the pot to the pendingWinnings mapping and immediately sets the pot state variable to 0. Only after the pot has been cleared is the GameEnded event emitted, using the now-zero pot variable as the prize amount argument.


function declareWinner() external gameNotEnded {
require(currentKing != address(0), "Game: No one has claimed the throne yet.");
require(
block.timestamp > lastClaimTime + gracePeriod,
"Game: Grace period has not expired yet."
);
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
@> pot = 0; // The pot is cleared here...
@> emit GameEnded(currentKing, pot, block.timestamp, gameRound); // ...but is used here, when its value is already 0.
}

Risk

Likelihood:

  • This issue will occur every time declareWinner() is successfully called, without exception.


Impact:

  • This bug does not lead to a direct loss of user funds, as the winner's prize is correctly credited to their pendingWinnings balance. However, it severely damages the contract's transparency and usability. The on-chain event log, which is a critical source of truth for all external applications, becomes permanently incorrect. Any front-end, data analytics platform, or block explorer relying on these events will misleadingly display that every winner won a prize of 0 ETH. This erodes user trust and breaks off-chain functionality.

Proof of Concept

This can be verified with a Foundry test that inspects emitted events. The test would simulate a game, advance time, declare a winner, and check the emitted GameEnded event's parameters.

  1. A player calls claimThrone() with 0.1 ETH. The pot becomes 0.095 ETH (after platform fees).

  2. The test uses vm.warp() to expire the grace period.

  3. The test calls declareWinner().

  4. The test uses vm.expectEmit to check the GameEnded event.

interface IGame {
event GameEnded(
address indexed winner,
uint256 prizeAmount,
uint256 timestamp,
uint256 round
);
}
function testDeclareWinner_EmitsZeroPrizeAmount() public {
// Player 1 claims the throne, pot becomes non-zero
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
uint256 actualPrize = game.pot();
assertTrue(actualPrize > 0);
// Expire the grace period
vm.warp(block.timestamp + GRACE_PERIOD + 1);
// CORRECTED: The check flags should match the event's indexed parameters.
// We check topic1 (winner) and data (the rest), but not topic2 or topic3.
vm.expectEmit(true, false, false, true);
// Define the expected event payload for the vm.expectEmit check.
// We assert that the emitted `prizeAmount` will be 0, proving the vulnerability.
emit GameEnded(player1, 0, block.timestamp, 1);
// Declare the winner
game.declareWinner();
}

Recommended Mitigation

Store the prize amount in a local memory variable before clearing the pot state variable. Use this local variable for both crediting the winner and emitting the event to ensure data integrity.

- pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
- pot = 0; // Reset pot after assigning to winner's pending winnings
-
- emit GameEnded(currentKing, pot, block.timestamp, gameRound);
+ uint256 prizeAmount = pot; // Store the prize amount in a local variable first.
+ pendingWinnings[currentKing] = pendingWinnings[currentKing] + prizeAmount;
+ pot = 0; // Reset pot after assigning to winner's pending winnings
+
+ emit GameEnded(currentKing, prizeAmount, block.timestamp, gameRound);
Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Game::declareWinner emits GameEnded event with pot = 0 always

Support

FAQs

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