Last Man Standing

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

GameEnded Event Emits Zeroed Reward

Root + Impact

The GameEnded event always emits pot after it has been reset to 0. This means the emitted event shows no prize, even though the winner actually receives the real pot amount in pendingWinnings

Description

  • Normal Behavior:
    When a winner is declared, the full pot balance should be assigned to the winner’s pendingWinnings and the GameEnded event should transparently log the amount won.

  • Specific Issue:
    The code assigns the pot to pendingWinnings, then resets the pot to 0, then emits GameEnded with pot — so the event always shows 0 instead of the true pot value.

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; // pot is zeroed
emit GameEnded(currentKing, pot, block.timestamp, gameRound); // @> pot here is zero
}

Risk

Likelihood:

  • Happens every time declareWinner() is called and the pot is non-zero.

  • No special conditions required — normal use case.

Impact:

  • Low impact: Funds are not lost.

  • But it misleads off-chain listeners (frontends, indexers, analytics, or audit logs) who rely on the event for the real prize value.

  • Reduces transparency of game outcomes.

Proof Of Code

.
.
.
event GameEnded(
address indexed winner,
uint256 prizeAmount,
uint256 timestamp,
uint256 round
);
.
.
.
function test_declareWinnerEmitsZeroReward() public {
vm.startPrank(player1);
uint256 claimFee = game.claimFee();
game.claimThrone{value: claimFee}();
vm.stopPrank();
vm.warp(block.timestamp + 2 days);
vm.expectEmit(address(game));
// emit GameEnded(currentKing, pot, block.timestamp, gameRound);
emit GameEnded(address(player1), 0, block.timestamp, game.gameRound());
game.declareWinner();
}

Result:

Traces:
[227572] GameTest::test_declareWinnerEmitsZeroPot()
├─ [0] VM::startPrank(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Return]
├─ [2514] Game::claimFee() [staticcall]
│ └─ ← [Return] 100000000000000000 [1e17]
├─ [150621] Game::claimThrone{value: 100000000000000000}()
│ ├─ emit ThroneClaimed(newKing: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], claimAmount: 100000000000000000 [1e17], newClaimFee: 110000000000000000 [1.1e17], newPot: 95000000000000000 [9.5e16], timestamp: 1)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::warp(172801 [1.728e5])
│ └─ ← [Return]
├─ [0] VM::expectEmit(Game: [0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b])
│ └─ ← [Return]
├─ [2471] Game::gameRound() [staticcall]
│ └─ ← [Return] 1
├─ emit GameEnded(winner: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], prizeAmount: 0, timestamp: 172801 [1.728e5], round: 1)
├─ [48689] Game::declareWinner()
│ ├─ emit GameEnded(winner: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], prizeAmount: 0, timestamp: 172801 [1.728e5], round: 1)
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 165.35ms (11.35ms CPU time)
Ran 1 test suite in 400.97ms (165.35ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

- emit GameEnded(currentKing, pot, block.timestamp, gameRound);
+ emit GameEnded(currentKing, pendingWinnings[currentKing], block.timestamp, gameRound);
Updates

Appeal created

inallhonesty Lead Judge about 1 month 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.