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.
function declareWinner() external gameNotEnded {
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
@> pot = 0;
@> emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}
Risk
Likelihood: High
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
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();
}
function test_PoC_GameEndedEventEmitsZeroPrize() public {
uint256 prize = 0.5 ether;
vm.warp(3 days);
bytes32 kingSlot = bytes32(uint256(1));
bytes32 lastClaimTimeSlot = bytes32(uint256(2));
bytes32 potSlot = bytes32(uint256(4));
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
vm.store(address(game), potSlot, bytes32(prize));
vm.store(address(game), lastClaimTimeSlot, bytes32(uint256(block.timestamp - 2 days)));
assertEq(game.pot(), prize, "Setup failed: Pot was not set correctly");
assertEq(game.currentKing(), player1, "Setup failed: King was not set correctly");
vm.recordLogs();
game.declareWinner();
Vm.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries.length, 1, "Expected one GameEnded event to be emitted");
(uint256 prizeAmount, , ) =
abi.decode(entries[0].data, (uint256, uint256, uint256));
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);
}