Last Man Standing

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

Equality operator blocks every legitimate throne claim

Root + Impact

Description

  • Normal Behavior: Any external account that sends at least the claimFee should become the new king, updating currentKing and resetting the grace period timer.

  • Specific Issue: The require(msg.sender == currentKing, ...) condition in claimThrone() incorrectly uses == instead of !=, causing every non-king caller to revert, preventing anyone from claiming the throne.

  • This makes the entire game unplayable as no one can ever claim the throne.

// Root cause in the codebase with @> marks to highlight the relevant section
// contracts/Game.sol : claimThrone()
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."); // @> BUG: Should be !=
// ...
}

Risk

Likelihood:

  • Occurs on every single external call to claimThrone() by a non-king player

  • 100% reproduction rate for normal game usage

Impact:

  • Game becomes completely non-functional - no player can ever claim the throne

  • Contract fails its core purpose entirely, Prevents accumulation of the pot and progression of game rounds.

  • All potential players are blocked from participating

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 Finding1POC is Test {
Game public game;
address public deployer;
address public player1;
address public player2;
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");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
/**
* @notice POC: Demonstrates that no player can claim the throne due to wrong equality check
*/
function testPOC_EqualityOperatorPreventsAllClaims() public {
console2.log("=== FINDING #1 POC: EQUALITY OPERATOR BLOCKS ALL CLAIMS ===");
console2.log("");
// Verify initial state - no king
assertEq(game.currentKing(), address(0));
assertEq(game.totalClaims(), 0);
console2.log("Initial king:", game.currentKing());
console2.log("Initial total claims:", game.totalClaims());
console2.log("");
// Player 1 attempts to claim throne
console2.log("Player 1 attempting to claim throne...");
vm.prank(player1);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// Verify state unchanged - claim failed
assertEq(game.currentKing(), address(0));
assertEq(game.totalClaims(), 0);
console2.log("Result: CLAIM FAILED - No king set");
console2.log("Current king after attempt:", game.currentKing());
console2.log("");
// Player 2 also fails
console2.log("Player 2 attempting to claim throne...");
vm.prank(player2);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// State still unchanged
assertEq(game.currentKing(), address(0));
assertEq(game.totalClaims(), 0);
console2.log("Result: CLAIM FAILED - No king set");
console2.log("");
// Even deployer (owner) cannot claim
console2.log("Deployer (owner) attempting to claim throne...");
vm.prank(deployer);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
console2.log("Result: EVEN OWNER CANNOT CLAIM");
console2.log("");
console2.log("VULNERABILITY CONFIRMED:");
console2.log("- No player can ever claim the throne");
console2.log("- Game is completely non-functional");
console2.log("- All ETH sent with claims is rejected");
console2.log("");
console2.log("ROOT CAUSE: require(msg.sender == currentKing) should be !=");
}
/**
* @notice POC: Shows the financial impact - no ETH can enter the contract
*/
function testPOC_NoETHCanEnterContract() public {
uint256 initialBalance = address(game).balance;
console2.log("Initial contract balance:", initialBalance);
// Multiple players try to claim with ETH
address[3] memory players = [player1, player2, deployer];
for (uint i = 0; i < players.length; i++) {
vm.prank(players[i]);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
}
// Contract balance unchanged - no ETH accepted
uint256 finalBalance = address(game).balance;
assertEq(finalBalance, initialBalance);
console2.log("Final contract balance:", finalBalance);
console2.log("ETH accepted by contract: 0");
// Pot and fees remain zero
assertEq(game.pot(), 0);
assertEq(game.platformFeesBalance(), 0);
}
/**
* @notice POC: Demonstrates the logic error clearly
*/
function testPOC_LogicErrorExplanation() public {
console2.log("=== LOGIC ERROR ANALYSIS ===");
console2.log("");
address currentKing = game.currentKing();
console2.log("Current king:", currentKing);
console2.log("Is current king address(0)?", currentKing == address(0));
console2.log("");
console2.log("BROKEN LOGIC:");
console2.log("require(msg.sender == currentKing) means:");
console2.log("- If currentKing is address(0), msg.sender must be address(0)");
console2.log("- But msg.sender is never address(0) for real users");
console2.log("- Therefore, requirement always fails");
console2.log("");
console2.log("CORRECT LOGIC should be:");
console2.log("require(msg.sender != currentKing) means:");
console2.log("- Only non-kings can claim the throne");
console2.log("- Prevents current king from claiming again");
console2.log("- Allows new players to challenge");
}
}
[PASS] testPOC_EqualityOperatorPreventsAllClaims() (gas: 156686)
Logs:
=== FINDING #1 POC: EQUALITY OPERATOR BLOCKS ALL CLAIMS ===
Initial king: 0x0000000000000000000000000000000000000000
Initial total claims: 0
Player 1 attempting to claim throne...
Result: CLAIM FAILED - No king set
Current king after attempt: 0x0000000000000000000000000000000000000000
Player 2 attempting to claim throne...
Result: CLAIM FAILED - No king set
Deployer (owner) attempting to claim throne...
Result: EVEN OWNER CANNOT CLAIM
VULNERABILITY CONFIRMED:
- No player can ever claim the throne
- Game is completely non-functional
- All ETH sent with claims is rejected
ROOT CAUSE: require(msg.sender == currentKing) should be !=
[PASS] testPOC_LogicErrorExplanation() (gas: 24283)
Logs:
=== LOGIC ERROR ANALYSIS ===
Current king: 0x0000000000000000000000000000000000000000
Is current king address(0)? true
BROKEN LOGIC:
require(msg.sender == currentKing) means:
- If currentKing is address(0), msg.sender must be address(0)
- But msg.sender is never address(0) for real users
- Therefore, requirement always fails
CORRECT LOGIC should be:
require(msg.sender != currentKing) means:
- Only non-kings can claim the throne
- Prevents current king from claiming again
- Allows new players to challenge
[PASS] testPOC_NoETHCanEnterContract() (gas: 139165)
Logs:
Initial contract balance: 0
Final contract balance: 0
ETH accepted by contract: 0

Recommended Mitigation

- remove this code
+ add this code
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.");
+ require(msg.sender != currentKing, "Game: You are already the king.");
// ... rest of function
}
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.