Eggstravaganza

First Flight #37
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Game State Persistence Across Sessions in EggHuntGame.sol

Vulnerability Summary

Description

The EggHuntGame contract does not reset critical game state variables between game sessions, namely:

  • eggCounter: the global counter tracking minted egg token IDs.

  • eggsFound: a mapping storing how many eggs each player has found.

As a result, starting a new game session after ending the previous one leads to the continuation of game progress, which violates the principle of session isolation and undermines fair competition.

Root Cause

The root issue lies in the lack of state resets in the startGame function. While this function initiates a new game by setting timestamps and marking gameActive = true, it does not reset:

  • eggCounter — causing token IDs to continue incrementing across sessions.

  • eggsFound — causes players’ prior scores to persist into the new session.

These variables are declared at the contract level and are not tied to any session identifier or scoped reset mechanism.

mapping(address => uint256) public eggsFound;
uint256 public eggCounter;

Proof of Concept (PoC)

The following test simulates two separate game sessions with different participants. It clearly shows that the state from the first session carries over into the second, creating misleading and cumulative stats.

function testStateVariablesPersistAcrossGameSessions() public {
// Start Game 1
game.startGame(3600);
game.setEggFindThreshold(100);
vm.startPrank(alice);
game.searchForEgg();
game.searchForEgg();
game.searchForEgg();
assertEq(game.eggsFound(alice), 3);
assertEq(game.eggCounter(), 3);
vm.stopPrank();
game.endGame();
vm.warp(block.timestamp + 1 days);
// Start Game 2
game.startGame(3600);
vm.startPrank(bob);
game.searchForEgg();
game.searchForEgg();
assertEq(game.eggsFound(bob), 2);
vm.stopPrank();
// eggCounter persists from previous session
assertEq(game.eggCounter(), 5);
// Alice’s score persists into new session
assertEq(game.eggsFound(alice), 3);
// Alice can still accumulate from prior score
vm.startPrank(alice);
game.searchForEgg();
assertEq(game.eggsFound(alice), 4);
assertEq(game.eggCounter(), 6);
vm.stopPrank();
}

Impact

The implications of this bug vary depending on how the game is intended to be played and rewarded:

Game Logic Integrity

  • Game results from a prior session persist, causing confusion and undermining trust in fairness.

  • Players who participated in earlier sessions retain unfair advantages over new participants.

Leaderboard & Scoring Inaccuracy

  • Aggregated stats across sessions skew game analytics and any leaderboards built on eggsFound.

Token ID Allocation

  • eggCounter continues incrementing, creating one continuous ID sequence across sessions. This may or may not be intended, but it should be explicitly clarified or corrected.

User Experience

  • Users expecting a fresh game each time will be confused or misled if their old scores or behaviors affect new sessions.

Recommendations

To fix this issue and enforce session isolation, consider the following remediations:

Option 1: Reset Game State in startGame()

function startGame(uint256 duration) external onlyOwner {
require(!gameActive, "Game already active");
require(duration >= MIN_GAME_DURATION, "Duration too short");
// Reset game state
eggCounter = 0;
// Clear all previous participant scores
// WARNING: This is only feasible for small or known sets of players.
// Otherwise consider Option 2 below
address[] memory players = previousPlayers;
for (uint256 i = 0; i < players.length; i++) {
eggsFound[players[i]] = 0;
}
delete previousPlayers;
startTime = block.timestamp;
endTime = block.timestamp + duration;
gameActive = true;
emit GameStarted(startTime, endTime);
}

⚠️ Note: You’d need to track previous participants in an array like address[] public previousPlayers, and append to it in searchForEgg().

Option 2: Session-Based Struct Refactor

Refactor the contract to track stats per session:

struct GameSession {
uint256 eggCounter;
mapping(address => uint256) eggsFound;
}
uint256 public sessionId;
mapping(uint256 => GameSession) public gameSessions;

Then, increment sessionId and work only within the current session mapping for each game.

Conclusion

Failing to reset session-related state variables causes data from previous games to persist, potentially skewing scoring logic, user expectations, and reward fairness. For games intended to have distinct rounds or competitions, this bug can significantly degrade the experience and integrity of gameplay. A scoped or resettable state design is strongly advised.

Updates

Lead Judging Commences

m3dython Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Incomplete end game handling

Incorrect values reported when a game is ended early

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.