Last Man Standing

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

Unreachable gameplay: wrong equality check bricks `claimThrone()`

Root + Impact

Description

  • Expected behavior: Any non-king can start/continue the game by calling claimThrone() with at least claimFee. The call should set currentKing = msg.sender, update lastClaimTime, add to the pot/fees, and increase claimFee(if applicable).

  • Actual behavior: claimThrone() requires the caller to already be the current king. At deployment, currentKing == address(0), and no EOA/contract equals address(0). Thus the first (and every) claim reverts, the game never starts, and all downstream mechanics are unreachable.

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
// @> BUG: this requires the caller to already be the king
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
// ... rest of function ...
}

Risk

Likelihood:

  • Occurs on every deployment immediately after construction: currentKing is defaulted to address(0) and cannot be matched by any caller.

  • Persists forever: No reachable state transition can set currentKing to a nonzero address, because every claimThrone() reverts. The only other assignment to currentKing is in resetGame(), which sets it back to address(0).

Impact:

  • Total denial of service for core functionality: no one can claim the throne and the game is unplayable.

  • All flows dead: declareWinner() (needs a king), withdrawWinnings() (no winnings ever accrue), and fee/pot accounting are never exercised.

Proof of Concept

Generality comes from the previous formal argument.

What this demonstrates: From a fresh deployment, a funded non-zero arbitrary address calls claimThrone with msg.value >= claimFee and reverts with the “already the king” message, currentKing remains address(0).

Steps to reproduce:

  1. Deploy Game with any valid parameters.

  2. Fund an arbitrary non-zero address.

  3. Call claimThrone{value: game.claimFee()} from that address.

  4. Observe revert: "Game: You are already the king. No need to re-claim." and currentKing == address(0).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameBrickTest is Test {
Game game;
uint256 constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 constant GRACE_PERIOD = 1 days;
uint256 constant FEE_INCREASE_PERCENTAGE = 10;
uint256 constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
}
function test_ClaimThrone_AlwaysReverts_FirstCall() public {
address p = address(0xBEEF);
vm.deal(p, 10 ether);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
vm.prank(p);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
assertEq(game.currentKing(), address(0));
}
}

Recommended Mitigation

Change the equality check so any non-king can claim. Only the current king is blocked from reclaiming.

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.");
+ require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
uint256 sentAmount = msg.value;
// ...
}

Optional

Use a custom error error Game__AlreadyKing() : if (msg.sender == currentKing) revert AlreadyKing(); for gas and clarity.

Follow-ups

Fixing this unlocks additional issues (e.g., event/accounting/spec mismatches). I will submit those separately as findings conditional on this fix.

Updates

Appeal created

inallhonesty Lead Judge 17 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Game::claimThrone `msg.sender == currentKing` check is busted

Support

FAQs

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