Last Man Standing

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

Missing Grace Period Check in claimThrone Allows Claims After Expiration

Description

The Game contract allows players to claim the throne by paying a claimFee, becoming the currentKing, with the game ending when the gracePeriod expires and a winner as the last player to claimFee when the grace period expires. The claimThrone function lacks a check to prevent claims after the gracePeriod has expired, allowing claims Even when graceperiods expires, violating the intended game mechanics.

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
@> // Missing check for gracePeriod expiration

Risk

Likelihood:

Players can call claimThrone after the gracePeriod expires, as the gameNotEnded modifier only checks gameEnded, which remains false until declareWinner is called.
No incentive or restriction prevents players from claiming the throne during the expired grace period, as the contract does not enforce a pause.

Impact:

Undermines game rules by allowing throne claims when the current king should be declared the winner, potentially delaying or manipulating the game’s outcome.
Increases pot and claimFee unexpectedly, disrupting fair distribution of winnings and platform fees.

Proof of Concept

Before running this test, Kindly change the clamThrone require to
require(msg.sender != currentKing);

function test_ClaimThrone_AfterGracePeriod() public {
//player1 starts the game by claiming the throne
vm.startPrank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
vm.stopPrank();
// Fast forward past grace period
vm.warp(block.timestamp + game.gracePeriod() + 1);
// Player2 attempts to claim throne
vm.startPrank(player2);
uint256 currentClaimFee = game.claimFee();
// Should revert, but currently succeeds
game.claimThrone{value: currentClaimFee}();
assertEq(game.currentKing(), player2, "Player2 should not be current king");
vm.stopPrank();
}

Tool Used

Manual Review

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

Recommended Mitigation

Add a check to check if the game has ended.

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
//You can Use this if You considered the block.number
+ require(block.number <= lastClaimBlock + gracePeriodBlocks, "Game: Grace period expired, declare winner instead.");
//Use this if you want to conynue using block.timestamp
+ require(block.timestamp <= lastClaimTime + gracePeriod, "Game: Grace period expired, declare winner instead.");
Updates

Appeal created

inallhonesty Lead Judge about 2 months 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.