Last Man Standing

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

`GameEnded` Event Emits Zero Prize Amount, Breaking Off-Chain Monitoring

Description

Events are a critical part of smart contracts, allowing off-chain applications, user interfaces, and monitoring tools to track the contract's state changes accurately. The GameEnded event is intended to announce the winner and the prize amount they have won.

However, the function declareWinner() first zeroes out the pot state variable and then emits the GameEnded event, passing the now-zero pot value as the prize amount. This results in the event always reporting a prizeAmount of 0, which is incorrect and misleading. Off-chain applications will fail to display the correct prize amount, causing confusion for users and breaking monitoring scripts.

// src/Game.sol
function declareWinner() external gameNotEnded {
// ... checks ...
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
@> pot = 0; // The pot is zeroed out BEFORE the event is emitted.
@> emit GameEnded(currentKing, pot, block.timestamp, gameRound); // 'pot' is now 0.
}

Risk

Likelihood: High

  • This incorrect event emission will happen every single time a winner is declared, without exception.

Impact: Low

  • This bug does not cause any direct financial loss, as the winner still receives the correct prize in their pendingWinnings balance.

  • However, it severely degrades the usability and monitorability of the protocol. User interfaces will display incorrect information, and any off-chain services or analytics relying on this event for accurate prize data will be broken.

Proof of Concept

The following Foundry test proves that the GameEnded event is emitted with an incorrect prizeAmount of 0. The test uses cheatcodes to manually set the game into a winning state to isolate and verify the flawed event emission in the declareWinner function.

Test File: test/Event.t.sol

// test/Event.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Vm} from "forge-std/Vm.sol";
import {Game} from "../../src/Game.sol";
contract EventTest is Test {
Game public game;
address public deployer;
address public player1;
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
vm.deal(deployer, 1 ether);
vm.deal(player1, 1 ether);
vm.startPrank(deployer);
game = new Game(0.1 ether, 1 days, 10, 5);
vm.stopPrank();
}
// NOTE: This test manually sets the game state to bypass the critical bug in `claimThrone`.
function test_PoC_GameEndedEventEmitsZeroPrize() public {
// --- Setup: Manually create a winning scenario using the correct storage slots ---
uint256 prize = 0.5 ether;
// Advance time to a reasonable value to prevent underflow issues with timestamps.
vm.warp(3 days);
// Correct Storage Slots from `forge inspect Game storage-layout --pretty`
bytes32 kingSlot = bytes32(uint256(1));
bytes32 lastClaimTimeSlot = bytes32(uint256(2));
bytes32 potSlot = bytes32(uint256(4));
// Set player1 as the currentKing
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
// Set the pot amount
vm.store(address(game), potSlot, bytes32(prize));
// Set the lastClaimTime to be in the past to allow winning
vm.store(address(game), lastClaimTimeSlot, bytes32(uint256(block.timestamp - 2 days)));
// Sanity check that our setup worked
assertEq(game.pot(), prize, "Setup failed: Pot was not set correctly");
assertEq(game.currentKing(), player1, "Setup failed: King was not set correctly");
// --- Execution & Assertion ---
vm.recordLogs();
game.declareWinner();
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 1, "Expected one GameEnded event to be emitted");
// Decode the non-indexed data from the event log
(uint256 prizeAmount, , ) =
abi.decode(entries[0].data, (uint256, uint256, uint256));
// This is the key assertion that proves the bug.
// The event emitted a prize amount of 0, even though the pot had a balance.
assertEq(prizeAmount, 0, "VULNERABILITY: prizeAmount in event was 0, but should have been the prize value.");
}
}

Execution Command & Successful Result:

$ forge test --match-path test/Event.t.sol
[PASS] test_PoC_GameEndedEventEmitsZeroPrize() (gas: 55900)
Suite result: ok. 1 passed; 0 failed; 0 skipped

The successful test confirms that prizeAmount in the emitted event is 0, validating the vulnerability.

Recommended Mitigation

To fix this, the value of the pot should be stored in a local variable before the state variable is zeroed out. This local variable should then be used in the event emission to ensure the correct prize amount is logged.

// src/Game.sol
function declareWinner() external gameNotEnded {
// ... (checks remain the same) ...
gameEnded = true;
+ uint256 prizeAmount = pot;
- pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
- pot = 0; // Reset pot after assigning to winner's pending winnings
+ pendingWinnings[currentKing] = pendingWinnings[currentKing] + prizeAmount;
+ pot = 0;
- emit GameEnded(currentKing, pot, block.timestamp, gameRound);
+ emit GameEnded(currentKing, prizeAmount, block.timestamp, gameRound);
}
Updates

Appeal created

inallhonesty Lead Judge 9 days 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.