Last Man Standing

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

Inverted Logic in claimThrone()

Root + Impact

Description

  • The claimThrone() function should allow any new player to claim the throne by sending the required ETH fee, while preventing the current king from claiming again unnecessarily. The intended flow is: (1) first player claims and becomes king, (2) subsequent players can claim the throne from the current king by paying a higher fee, (3) each successful claim updates the king and increases the fee for the next claim, creating a competitive bidding mechanism.

  • The require statement on line 132 contains inverted logic that completely breaks the game from deployment. The condition require(msg.sender == currentKing, ...) prevents anyone except the current king from claiming the throne, but since currentKing starts as address(0) and no real address can equal address(0), nobody can ever make the first claim. This creates a permanent denial of service where 100% of users are blocked from the core game functionality, making the contract completely unusable for its intended purpose. The logic should be msg.sender != currentKing to allow new players to claim while preventing the current king from reclaiming.

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

Risk

Likelihood:

  • This vulnerability occurs immediately upon contract deployment when any user attempts to interact with the core claimThrone() function, as the initial currentKing value of address(0) will never equal any real user's address

  • This affects every single user interaction with the primary game mechanic, occurring on 100% of claim attempts by any address in the system with no exceptions or workarounds possible

Impact:

  • Complete denial of service where the entire game becomes permanently unusable from deployment, preventing any player from ever claiming the throne and rendering all intended functionality inaccessible

  • All users waste gas costs on failed transactions while being unable to participate in the game, creating negative user experience and potential reputation damage as the contract appears broken or fraudulent

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract RefinedCriticalInvertedLogicPoC is Test {
Game public game;
// Test actors
address public deployer;
address public alice;
address public bob;
address public attacker;
// Game parameters
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 public constant GRACE_PERIOD = 1 days;
uint256 public constant FEE_INCREASE_PERCENTAGE = 10;
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
deployer = makeAddr("deployer");
alice = makeAddr("alice");
bob = makeAddr("bob");
attacker = makeAddr("attacker");
vm.deal(deployer, 10 ether);
vm.deal(alice, 5 ether);
vm.deal(bob, 5 ether);
vm.deal(attacker, 5 ether);
// Deploy the vulnerable Game contract
vm.prank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
console2.log("=== CRITICAL-001 REFINED PoC: Complete Game DoS ===");
console2.log("Vulnerability: Game is broken from deployment");
console2.log("Impact: NOBODY can EVER claim the throne");
console2.log("");
}
/**
* @notice Proves the game is completely broken from deployment
* @dev Shows that even the first claim attempt fails
*/
function test_CRITICAL_GameCompletelyBroken() public {
console2.log("=== DEMONSTRATING COMPLETE GAME FAILURE ===");
// Verify initial state
console2.log("Initial State Analysis:");
console2.log("currentKing:", game.currentKing());
console2.log("claimFee:", game.claimFee());
console2.log("gameEnded:", game.gameEnded());
// Confirm currentKing is address(0)
assertEq(game.currentKing(), address(0), "currentKing should be address(0)");
console2.log("");
console2.log("ATTEMPT 1: Alice tries first claim");
console2.log("Expected: Should work (this is the INTENDED first claim)");
console2.log("Actual: FAILS due to inverted logic");
// Alice attempts the very first claim - this should work but fails
vm.prank(alice);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("Result: FIRST CLAIM FAILED");
console2.log("Reason: msg.sender (alice) != currentKing (address(0))");
console2.log("");
// Verify Alice is NOT the king (game state unchanged)
assertEq(game.currentKing(), address(0), "King should still be address(0)");
console2.log("ATTEMPT 2: Bob tries to claim");
vm.prank(bob);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("Result: SECOND CLAIM FAILED");
console2.log("");
console2.log("ATTEMPT 3: Even address(0) cannot claim (if it could)");
console2.log("Logic analysis: require(address(0) == address(0)) would pass");
console2.log("But address(0) cannot send transactions");
console2.log("");
console2.log("=== FINAL STATE ===");
console2.log("currentKing remains:", game.currentKing());
console2.log("Total successful claims:", game.totalClaims());
console2.log("Game functionality: COMPLETELY BROKEN");
}
/**
* @notice Demonstrates the transaction revert behavior
* @dev Shows how the contract rejects all claim attempts
*/
function test_CRITICAL_TransactionReverts() public {
console2.log("=== TRANSACTION REVERT DEMONSTRATION ===");
console2.log("All claimThrone() attempts revert immediately");
console2.log("");
uint256 contractBalanceBefore = address(game).balance;
console2.log("Contract balance before any attempts:", contractBalanceBefore);
console2.log("");
// Alice's attempt reverts
console2.log("Alice attempting claim...");
vm.prank(alice);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("Alice's transaction REVERTED as expected");
// Bob's attempt also reverts
console2.log("Bob attempting claim...");
vm.prank(bob);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("Bob's transaction REVERTED as expected");
uint256 contractBalanceAfter = address(game).balance;
console2.log("");
console2.log("Contract balance after attempts:", contractBalanceAfter);
// Contract balance unchanged due to reverts
assertEq(contractBalanceAfter, contractBalanceBefore, "Contract balance unchanged due to reverts");
console2.log("");
console2.log("IMPACT ANALYSIS:");
console2.log("- All user interactions fail immediately");
console2.log("- No ETH gets locked (transactions revert)");
console2.log("- Users waste gas on failed transactions");
console2.log("- Game appears broken to all users");
console2.log("- Reputation damage: Users think contract is scam");
}
/**
* @notice Shows the exact logical error and its fix
* @dev Provides clear before/after comparison
*/
function test_CRITICAL_LogicalErrorAnalysis() public {
console2.log("=== LOGICAL ERROR BREAKDOWN ===");
console2.log("");
console2.log("CURRENT BROKEN CODE (Line 132):");
console2.log("require(msg.sender == currentKing, \"Game: You are already the king.\");");
console2.log("");
console2.log("EXECUTION ANALYSIS:");
console2.log("1. currentKing = address(0) (initial state)");
console2.log("2. Alice calls claimThrone(), msg.sender = alice's address");
console2.log("3. Condition: alice's address == address(0) -> FALSE");
console2.log("4. require(FALSE) -> REVERT");
console2.log("");
console2.log("INTENDED LOGIC:");
console2.log("- Prevent current king from claiming again");
console2.log("- Allow NEW players to claim throne");
console2.log("");
console2.log("CORRECT FIX:");
console2.log("require(msg.sender != currentKing, \"Game: You are already the king.\");");
console2.log("");
console2.log("FIXED EXECUTION FLOW:");
console2.log("1. currentKing = address(0) (initial state)");
console2.log("2. Alice calls claimThrone(), msg.sender = alice's address");
console2.log("3. Condition: alice's address != address(0) -> TRUE");
console2.log("4. require(TRUE) -> CONTINUE EXECUTION");
console2.log("5. Alice becomes the new king");
console2.log("");
// Demonstrate the current broken state
assertEq(game.currentKing(), address(0), "King is address(0)");
vm.prank(alice);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("CONFIRMED: Current logic prevents ANY player from claiming");
}
/**
* @notice Demonstrates this affects ALL possible users
* @dev Shows systematic failure across multiple actors
*/
function test_CRITICAL_SystematicFailure() public {
console2.log("=== SYSTEMATIC FAILURE ACROSS ALL USERS ===");
address[10] memory testUsers;
for (uint i = 0; i < 10; i++) {
testUsers[i] = makeAddr(string(abi.encodePacked("user", i)));
vm.deal(testUsers[i], 1 ether);
}
console2.log("Testing 10 different users attempting to claim...");
for (uint i = 0; i < 10; i++) {
console2.log("User", i, "attempting claim...");
vm.prank(testUsers[i]);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// Verify no state change
assertEq(game.currentKing(), address(0), "King should remain address(0)");
}
console2.log("");
console2.log("RESULT: All 10 users failed to claim throne");
console2.log("IMPACT: 100% failure rate across entire user base");
console2.log("SEVERITY: Complete system failure");
}
/**
* @notice Shows this breaks all game mechanics, not just claiming
* @dev Demonstrates cascade failure across game features
*/
function test_CRITICAL_CascadeFailure() public {
console2.log("=== CASCADE FAILURE ANALYSIS ===");
console2.log("Since nobody can claim, ALL game features break:");
console2.log("");
// Cannot declare winner (no king exists)
console2.log("1. Testing declareWinner()...");
vm.expectRevert("Game: No one has claimed the throne yet.");
game.declareWinner();
console2.log(" FAILED: No king to declare as winner");
// Cannot withdraw winnings (no winnings exist)
console2.log("2. Testing withdrawWinnings()...");
vm.prank(alice);
vm.expectRevert("Game: No winnings to withdraw.");
game.withdrawWinnings();
console2.log(" FAILED: No winnings accumulated");
// Game state remains completely static
console2.log("3. Checking game state progression...");
assertEq(game.totalClaims(), 0, "No claims ever succeeded");
assertEq(game.pot(), 0, "No pot accumulated");
assertEq(game.playerClaimCount(alice), 0, "Alice has zero successful claims");
console2.log(" CONFIRMED: Zero progression in game state");
console2.log("");
console2.log("=== FINAL IMPACT ASSESSMENT ===");
console2.log("Functionality Available: 0%");
console2.log("User Experience: Completely Broken");
console2.log("Financial Risk: Gas Loss on Every Interaction");
console2.log("Reputational Impact: Total System Failure");
console2.log("Classification: CRITICAL SEVERITY - IMMEDIATE FIX REQUIRED");
}
}

This Poc demonstartes:

  1. The exact logic error that causes immediate failure on first claim attempt

  2. Universal impact across all users with 100% failure rate

  3. Cascade failure where all dependent game features become inaccessible


Recommended Mitigation

The fix changes the equality operator from == to != to implement the correct logic. This allows new players to claim the throne (since their address will not equal the current king's address) while still preventing the current king from claiming again unnecessarily.

- 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 4 months 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.

Give us feedback!