Last Man Standing

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

Game Contract Locked Due to Incorrect Access Control in `claimThrone()`

Bug Report: Logic Flaw in claimThrone() Function Prevents All Gameplay

Description

  • Normal Behavior: Players should be able to claim the throne by sending ETH >= claimFee, becoming the new currentKing. Previous king receives a payout, and the fee increases for the next claim.

  • Issue: Due to an inverted condition in claimThrone(), the contract rejects ALL claims because it incorrectly requires the caller to be the current king — which is logically impossible for new players, when currentKing is initially address(0).

function claimThrone() external payable gameNotEnded nonReentrant {
require(
msg.sender == currentKing, // @> Logic flaw: Should be `!=`
"Game: You are already the king. No need to re-claim."
);
// ...
}

Risk

Likelihood:

  • 100% Reproducibility: Every claimThrone() call reverts until the contract is fixed. No edge cases.

  • Immediate Failure: The issue manifests on the very first interaction, preventing further gameplay.

Impact:

  • Game Bricked: Core functionality is completely broken. No throne claims possible.

  • User Trust & Reputation Damage: The project appears non-functional, which may erode user confidence even though funds remain safe.

Proof of Concept

Here’s a Foundry test script which can be run against a local Ethereum fork.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameTest {
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();
}
function test_ClaimThroneReverts() public {
address testUser = player1; // any EOA attempting to claim the throne
vm.deal(testUser, 1 ether);
vm.prank(testUser);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: 1 ether}();
console2.log("Current King: ", game.currentKing());
assertEq(game.currentKing(), address(0));
}
}
  • This test reverts 100% of the time due to incorrect require() logic.

  • Even after sending valid ETH, currentKing remains address(0) and no player becomes king.

Recommended Mitigation

function claimThrone() external payable gameNotEnded nonReentrant {
require(
- msg.sender == currentKing,
+ msg.sender != currentKing,
"Game: You are already the king. No need to re-claim."
);
// ...
}
  • Add explicit validation for currentKing != address(0) in payout logic.

Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

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

kode_n_rolla Submitter
about 2 months ago
inallhonesty Lead Judge
about 1 month ago
inallhonesty Lead Judge about 1 month 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.