Last Man Standing

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

Deadlock if No Claims in Round 0

Root + Impact

Description

  • Normal behavior:
    The game should allow the owner to reset the round after the grace period, even if no one has claimed the throne in the first round. If no one claims, the game should not become permanently stuck.

    Specific issue:
    In the constructor, lastClaimTime is set to block.timestamp even though currentKing is address(0). If no one claims the throne, after the grace period expires:

    • declareWinner() cannot be called (because currentKing == address(0)).

    • resetGame() cannot be called (because gameEnded is still false). The contract is now deadlocked: neither players nor the owner can progress the game or recover funds.

// Root cause in the codebase with @> marks to highlight the relevant section// src/Game.sol
constructor(
uint256 _initialClaimFee,
uint256 _gracePeriod,
uint256 _feeIncreasePercentage,
uint256 _platformFeePercentage
) Ownable(msg.sender) {
// ...existing code...
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
lastClaimTime = block.timestamp; // @> Vulnerability: Should be 0, not block.timestamp
gameRound = 1;
gameEnded = false;
// currentKing starts as address(0) until first claim
}

Risk

Likelihood:

  • This will occur every time the contract is deployed and no one claims the throne in the first round.

Impact:

  • The contract becomes permanently stuck and unusable.

  • The owner cannot reset the game or recover any ETH sent to the contract.

Proof of Concept

The following test script demonstrates the bug. It deploys the contract, fast-forwards time past the grace period, and shows that neither declareWinner() nor resetGame() can be called if no one has claimed the throne.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameC2DeadlockTest is Test {
Game public game;
address public owner;
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 public constant GRACE_PERIOD = 1 days;
uint256 public constant FEE_INCREASE_PERCENTAGE = 10;
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
owner = makeAddr("owner");
vm.deal(owner, 1 ether);
vm.startPrank(owner);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
function test_Deadlock_NoClaimsInRound0() public {
// Fast forward past the grace period
vm.warp(block.timestamp + GRACE_PERIOD + 1);
// declareWinner should revert (no king)
vm.expectRevert("Game: No one has claimed the throne yet.");
game.declareWinner();
// resetGame should revert (game not ended)
vm.startPrank(owner);
vm.expectRevert("Game: Game has not ended yet.");
game.resetGame();
vm.stopPrank();
}
}

Recommended Mitigation

Set lastClaimTime = 0 in the constructor. Only set lastClaimTime when the first successful claim is made.

- lastClaimTime = block.timestamp; // Game starts immediately upon deployment
+ lastClaimTime = 0; // Timer starts on first claim

And in claimThrone(), add:

if (lastClaimTime == 0) {
lastClaimTime = block.timestamp;
}```
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Game gets stuck if no one claims

Support

FAQs

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