Root + Impact
Description
-
The normal behavior is that when a winner is declared via declareWinner()
, the contract emits a GameEnded
event showing the winner’s address, the prize amount won (the pot
), the timestamp, and the round number.
-
The issue is that the contract resets the pot
to zero before emitting the GameEnded
event, causing the event to always log a prize amount of zero instead of the actual accumulated pot.
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;
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}
Risk
Likelihood:
This will occur every time declareWinner()
is called to end a game round.
Since the event is emitted after the pot is cleared, the issue is consistent and reproducible on every winner declaration.
Impact:
Off-chain event listeners, UIs, and analytics relying on the GameEnded
event will always see zero as the prize amount, misrepresenting the actual winnings.
This may cause confusion or incorrect reward tracking in frontends or third-party tools, reducing trust and usability.
Proof of Concept
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Game.sol";
contract GameEventBugTest is Test {
Game game;
address player = address(0xBEEF);
function setUp() public {
game = new Game(1 ether, 60, 10, 5);
vm.deal(player, 10 ether);
}
function test_EventPrizeIsZeroDueToPotReset() public {
vm.prank(player);
game.claimThrone{value: 1 ether}();
vm.warp(block.timestamp + 61);
vm.expectEmit(true, true, true, true);
emit Game.GameEnded(player, 0, block.timestamp, 1);
game.declareWinner();
assertEq(game.pot(), 0);
}
}
Recommended Mitigation
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;
-
- emit GameEnded(currentKing, pot, block.timestamp, gameRound); // <-- event emitted after pot reset
+ emit GameEnded(currentKing, pot, block.timestamp, gameRound); // <-- emit event BEFORE pot reset
+
+ pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
+ pot = 0;
}