Last Man Standing

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

Incorrect Access Control Logic in `Game::claimThrone()` allows **no** Player to ever claim the throne, severely making the game unplayable.

Root + Impact

Description

In Game::claimThrone(), the access control require statement:

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

is logically inverted in that it only allows the Game::currentKing to claim throne and reverts if anyone else tries.
According to the game rules, part of the Game::currentKing limitations is that they "Cannot claim if they are already the current king.". However the above statement does the opposite and only allow the currentKing to claim throne.

Additionally when the game is initialized, Game::currentKing is address(0), further preventing anyone from participating, which severely breaks the game's protocol.

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

Risk

Likelihood:

  • Reason 1 : It occurs whenever any player tries to claim throne.

Impact:

  • On deployment, the initial currentKing is set to address(0).

  • Any user trying to claim the throne (e.g., msg.sender != address(0)) will fail the require statement, and the transaction reverts.

  • This makes it impossible for anyone to ever claim the throne, rendering the game non-functional.

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();
}
function testAnyoneCanPlay() public {
vm.prank(player1);
vm.expectRevert();
game.claimThrone{value: INITIAL_CLAIM_FEE}();// player attempts to claim throne, but transaction reverts due to checker
}
}

Recommended Mitigation

in Game::claimThrone(), replace the require statement.

function claimThrone() external payable gameNotEnded nonReentrant {
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."
);
.
.
.
}
Updates

Appeal created

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.