Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Owner Overwrites Active `gracePeriod` Instead of Baseline, Allowing Indefinite Game Freeze

Root + Impact

Root Cause: updateGracePeriod writes directly to the live gracePeriod used by declareWinner() instead of the initial template initialGracePeriod, with no upper bound → Impact: Owner can set an effectively infinite deadline, blocking game conclusion forever.

WHY WOULD ANYONE TRUST THE OWNER: It's a Game something similar to a gamble. So we can't expect the owner to be fair. The owner can change the rules at any time, so we can't expect the game to be fair.

Description

  • The updateGracePeriod(uint256 _newGracePeriod) function in Game.sol is intended to adjust the default timing for future rounds,

  • but it immediately overwrites the active gracePeriod variable used by the current round’s declareWinner() logic. With no maximum limit, the owner can set gracePeriod so large that:

@> require(block.timestamp > lastClaimTime + gracePeriod, "Game: Grace period has not expired yet.");
function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
@> // ❌ Directly overrides active gracePeriod rather than initialGracePeriod
@> // ❌ No upper bound check—any uint256 accepted
gracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}

Risk

Likelihood: High

  • Never becomes true, permanently preventing declareWinner() from succeeding. This action effectively freezes the game, locking users out and trapping the pot indefinitely.

  • Active Lock: Because gracePeriod governs the current round, updating it mid-round alters the deadline in real time.

  • Unbounded: Without a cap, _newGracePeriod can be set near 2^256-1, ensuring the grace check in declareWinner() always fails.

Impact: High

  • Permanent Lock: No winner can ever be declared; the game is stuck in limbo.

  • Trapped Funds: The accumulated pot remains inaccessible.

  • User Frustration: Players lose confidence when the game never resolves.

  • Single-Point Abuse: A malicious or compromised owner can sabotage the entire contract.

Similar fate befell early “last man standing” games on Ethereum, where misordered admin functions or unbounded parameters allowed operators to halt play and seize or lock up funds.

Tools Used:

  • Foundry Test Suite

  • Chat-GPT AI Assistance (Report Grammar Check & Improvements)

  • Manual Review

Proof of Concept

function test_owner_can_freeze_the_game_indefinitely() public {
vm.startPrank(player1);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
vm.startPrank(player3);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
vm.startPrank(deployer);
game.claimThrone{value: game.claimFee()}();
// Freeze the game by setting infinite grace period
game.updateGracePeriod(type(uint256).max - block.timestamp);
vm.stopPrank();
// player4 tries to claim the throne and/or declare the winner
vm.startPrank(player4);
// @notice: you have to fix the claimThrone bug
// that is currently missing grace period ends check
// to test the revert below...
// vm.expectRevert();
// game.claimThrone{value: game.claimFee()}();
vm.expectRevert("Game: Grace period has not expired yet.");
game.declareWinner();
vm.stopPrank();
}

step 1: go to test/Game.t.sol file

step 2: paste the above code ⬆️

step 3: run the test suite

forge test --mt test_owner_can_freeze_the_game_indefinitely

step 4: See the Output

Scenario:

  1. Multiple Claims

    • Player1, Player2, Player3 sequentially call claimThrone(), updating lastClaimTime each time.

  2. Owner Freeze

    • The owner then calls:

      game.updateGracePeriod(type(uint256).max - block.timestamp);
  3. Blocked Conclusion

    • When any player calls:

      game.declareWinner();

      it reverts with

      "Game: Grace period has not expired yet."
    • Since lastClaimTime + gracePeriod exceeds any realistic timestamp, the game is frozen indefinitely.

Recommended Mitigation

Update initialGracePeriod, Not Active

function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
- gracePeriod = _newGracePeriod;
+ initialGracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}
– This ensures only future rounds use the new interval.

Enforce an Upper Bound

```solidity
uint256 constant MAX_GRACE_PERIOD = 7 days;
require(_newGracePeriod <= MAX_GRACE_PERIOD, "Game: Grace period too long.");
```

Prevent Mid-Round Changes

```solidity
require(gameEnded, "Game: Cannot update during an active round.");
```
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
theirrationalone Submitter
about 1 month ago
theirrationalone Submitter
about 1 month ago
theirrationalone Submitter
about 1 month ago
inallhonesty Lead Judge
30 days ago
inallhonesty Lead Judge 26 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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