Last Man Standing

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

[L-2] Incorrect Prize Emission in `declareWinner()` (Data Corruption)

Description

The declareWinner() function emits incorrect prize amount in GameEnded event:

pendingWinnings[currentKing] += pot; // pot value captured here
pot = 0; // pot reset to 0
emit GameEnded(currentKing, pot, ...); // Emits 0 instead of actual prize

Risk

Impact:

  • Frontends/analytics will display wrong prize amount (0)

  • Discrepancy between actual winnings and logged event

  • Breaks prize transparency

Likelihood:

  • Affects every winner declaration

  • Persistent data corruption in event logs

Proof of Concept

Key Test Behaviors:

  1. Initial Setup

    • Creates a player who becomes king with a known pot amount

    • Captures the actual pot value (e.g., 0.95 ETH from a 1 ETH claim after 5% fee)

  2. Time Manipulation

    • Fast-forwards past grace period using vm.warp()

    • Ensures winner declaration is possible

  3. Event Verification

    • Explicitly expects emission of 0 as prize amount

    • Will fail if contract emits correct pot value

    • Uses Foundry's expectEmit for precise event matching

function testPrizeEmissionIncorrect() public {
address player = makeAddr("player");
vm.deal(player, 1 ether);
uint256 initialClaimFee = game.claimFee();
// Setup game with existing king and pot
vm.prank(player);
game.claimThrone{value: initialClaimFee}();
uint256 actualPot = game.pot(); // Store actual pot value
// Fast-forward past grace period
vm.warp(block.timestamp + game.gracePeriod() + 1);
uint256 gameRound = game.gameRound();
// Declare winner
vm.expectEmit(true, true, true, true);
emit GameEnded(player, 0, block.timestamp, gameRound); // Should emit actual pot
game.declareWinner();
assertNotEq(actualPot, 0, "Pot value is wrong");
}

Logs

forge test --mt testPrizeEmissionIncorrect -vvvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.26
[⠑] Solc 0.8.26 finished in 488.01ms
Compiler run successful!
Ran 1 test for test/Game.t.sol:GameTest
[PASS] testPrizeEmissionIncorrect() (gas: 190824)
Traces:
[230624] GameTest::testPrizeEmissionIncorrect()
├─ [0] VM::addr(<pk>) [staticcall]
│ └─ ← [Return] player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C]
├─ [0] VM::label(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], "player")
│ └─ ← [Return]
├─ [0] VM::deal(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [2514] Game::claimFee() [staticcall]
│ └─ ← [Return] 100000000000000000 [1e17]
├─ [0] VM::prank(player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C])
│ └─ ← [Return]
├─ [150601] Game::claimThrone{value: 100000000000000000}()
│ ├─ emit ThroneClaimed(newKing: player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], claimAmount: 100000000000000000 [1e17], newClaimFee: 110000000000000000 [1.1e17], newPot: 95000000000000000 [9.5e16], timestamp: 1)
│ └─ ← [Stop]
├─ [515] Game::pot() [staticcall]
│ └─ ← [Return] 95000000000000000 [9.5e16]
├─ [2536] Game::gracePeriod() [staticcall]
│ └─ ← [Return] 86400 [8.64e4]
├─ [0] VM::warp(86402 [8.64e4])
│ └─ ← [Return]
├─ [2471] Game::gameRound() [staticcall]
│ └─ ← [Return] 1
├─ [0] VM::expectEmit(true, true, true, true)
│ └─ ← [Return]
├─ emit GameEnded(winner: player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], prizeAmount: 0, timestamp: 86402 [8.64e4], round: 1)
├─ [46689] Game::declareWinner()
│ ├─ emit GameEnded(winner: player: [0x44E97aF4418b7a17AABD8090bEA0A471a366305C], prizeAmount: 0, timestamp: 86402 [8.64e4], round: 1)
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.20ms (190.75µs CPU time)
Ran 1 test suite in 5.20ms (1.20ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

function declareWinner() external gameNotEnded {
require(currentKing != address(0));
require(block.timestamp > lastClaimTime + gracePeriod);
gameEnded = true;
+ uint256 prize = pot;
pendingWinnings[currentKing] += pot;
pot = 0;
- emit GameEnded(currentKing, pot, block.timestamp, gameRound);
+ emit GameEnded(currentKing, prize, block.timestamp, gameRound);
}

Fix Benefits:

  1. Preserves actual prize amount in logs

  2. Maintains event emission after state changes

  3. Single-line change with no side effects

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.