Last Man Standing

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

Missing grace period validation allows victory theft after expiration

Description:

The claimThrone() function fails to validate whether the grace period has expired before allowing new throne claims. This creates a critical vulnerability where legitimate winners can have their victory stolen by late claimants.

The current claimThrone() implementation only checks:

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: Grace period expiration check
// ... continues with throne claim logic
}

The function lacks validation that block.timestamp < lastClaimTime + gracePeriod, allowing throne claims even after the current king should have already won.

Attack path:

  1. Alice claims throne: Calls claimThrone() and becomes currentKing at timestamp 1000

  2. Grace period passes: Grace period of 3600 seconds expires at timestamp 4600

  3. Alice is rightful winner: According to game rules, Alice should win the pot

  4. No winner declaration: No one calls declareWinner()

  5. Bob steals victory: At timestamp 4700, Bob calls claimThrone() and successfully becomes the new king

  6. Alice loses rightfully won prize: Alice's legitimate victory window is bypassed

Impact:

Legitimate winners lose rightfully earned prizes to late attackers

Fundamental game mechanics are broken - grace periods become meaningless


PoC:

Change this line in claimThore() for sucessful test run

- require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
+ require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");

Put this into Game.t.sol file and run forge test --mt testEnterTheGameAfterGracePeriod -vvv in the terminal

function testEnterTheGameAfterGracePeriod() public {
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
vm.warp(block.timestamp + GRACE_PERIOD + 1 hours);
vm.prank(player2);
game.claimThrone{value: INITIAL_CLAIM_FEE + (INITIAL_CLAIM_FEE * FEE_INCREASE_PERCENTAGE) / 100}();
assertEq(game.currentKing(), player2);
}

Recommended Mitigation:

Add grace period validation to the claimThrone() function to prevent claims after expiration:

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.");
// Add grace period validation
if (currentKing != address(0)) {
require(block.timestamp < lastClaimTime + gracePeriod,
"Game: Grace period expired. Call declareWinner first.");
}
// ... rest of function logic
}

This ensures that:

  1. Once the grace period expires, no new throne claims are possible

  2. declareWinner() must be called to properly end the game

  3. Legitimate winners cannot have their victories stolen

  4. Game rules are consistently enforced

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.