Last Man Standing

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

Throne can be claimed even after gracePeriod has ended, which can causethe game to run infinitely

Root + Impact

Description

  • The Game::claimThrone() function allows new players to claim the throne even after the grace period has elapsed. As a result, the game never truly ends until Game::declareWinner is called.

  • There is no check inside Game::claimThrone() to verify whether the current timestamp has exceeded the lastClaimTime + gracePeriod, which should signal the game's conclusion.

  • The modifier gameNotEnded only checks the gameEnded boolean — but since gameEnded is never automatically updated based on time, the condition never becomes true even when the grace period expires.

  • This allows players to indefinitely restart the game, as long as Game::declareWinner() is called.

https://github.com/CodeHawks-Contests/2025-07-last-man-standing/blob/47d9d19a78acb52270269f4bff1568b87eb81a96/src/Game.sol#L186

Risk

Likelihood:

  • Reason 1 : Throne can be claimed both during and after the grace period has elapsed

Impact:

  • The game can srun infintely if no winner is declared to mark the end of the game

  • This vulnerability is a seriousviolation of the game policy and mechanics, as the rightful king may not be preserved after the grace period has elapsed

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameTest is Test {
Game public game;
address public deployer;
address public player1;
address public player2;
address public player3;
address public maliciousActor;
// Initial game parameters for testing
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether; // 0.1 ETH
uint256 public constant GRACE_PERIOD = 1 days; // 1 day in seconds
uint256 public constant FEE_INCREASE_PERCENTAGE = 10; // 10%
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5; // 5%
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
player3 = makeAddr("player3");
maliciousActor = makeAddr("maliciousActor");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.deal(player3, 10 ether);
vm.deal(maliciousActor, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
modifier loadGame() {
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}(); // Player1 claim throne
console.log(game.currentKing());
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}(); // Player2 claim throne
vm.stopPrank();
_;
}
function testThroneCanBeClaimedAfterGracePeriod() public loadGame {
vm.warp(block.timestamp + GRACE_PERIOD + 1); // simulate time passage of the grace period
vm.startPrank(player3);
game.claimThrone{value: game.claimFee()}(); // new user claims throne after grace period has elapsed. This should not be possible
assertEq(game.currentKing(), player3);
}
}

Recommended Mitigation

  • Update the Game::claimThrone() so that it checks if the grace period has elapsed before a new player can claim throne.

function claimThrone() external payable gameNotEnded nonReentrant {
.
.
.
+ require(block.timestamp < lastClaimTime + gracePeriod );
.
.
.
}
Updates

Appeal created

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

Game::claimThrone can still be called regardless of the grace period

Support

FAQs

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