Last Man Standing

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

Only the currentKing can call claimThrone(), defeats whole purpose of the game

Root + Impact

Description

  • The claimThrone() function includes a condition (require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.")) that restricts execution to the current king. Since currentKing is initialized to address(0), and no valid msg.sender can equal address(0), the function reverts for all callers, preventing any player from claiming the throne. This renders the game unplayable from the start.

The funtion to claimThrone incorrectly has a requirement that the participant (msg.sender) is the currentKing.
this is incorrect, the condition should be that the participant is not the currentKing.
// Root cause in the codebase
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");

Risk

Likelihood:

  • The issue will arise when any player who attempts to call the claimThrone() function after a king has been established. The issue will manifest in all games. So likelihood is 100%


Impact:

  • The bug completely breaks the game’s core mechanic, which relies on players competing to claim the throne by paying the claimFee. No player can become the currentKing, so the game cannot progress, the pot cannot grow, and no claims can occur.

  • In a deployed contract, this would render the contract non-functional, leading to zero engagement, loss of user trust, and potential financial losses for users attempting to participate.

Proof of Concept

// SPDX-License-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../src/Game.sol";
contract GameTest is Test {
Game game;
address owner = address(0x1);
address playerA = address(0x2);
address playerB = address(0x3);
function setUp() public {
// Deploy the Game contract with parameters matching test/Game.t.sol
vm.prank(owner);
game = new Game(0.1 ether, 1 days, 10, 5); // initialClaimFee = 0.1 ETH, gracePeriod = 1 day, feeIncrease = 10%, platformFee = 5%
// Fund players with ETH
vm.deal(owner, 10 ether);
vm.deal(playerA, 10 ether);
vm.deal(playerB, 10 ether);
}
function testInitialState() public {
console.log("Initial claimFee:", game.claimFee() / 1e18, "ETH");
console.log("Initial currentKing:", game.currentKing());
console.log("Game ended:", game.gameEnded());
assertEq(game.currentKing(), address(0), "Initial currentKing should be address(0)");
}
function testClaimThroneBug() public {
// Log initial state
console.log("Initial currentKing:", game.currentKing());
console.log("Initial claimFee:", game.claimFee() / 1e18, "ETH");
// Player A attempts to claim the throne (should revert)
console.log("Player A attempting to claim, currentKing:", game.currentKing());
vm.prank(playerA);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: 0.1 ether}();
// Player B attempts to claim (should also revert)
console.log("Player B attempting to claim, currentKing:", game.currentKing());
vm.prank(playerB);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: 0.1 ether}();
// Verify no king is set
assertEq(game.currentKing(), address(0), "No king should be set due to bug");
assertEq(game.totalClaims(), 0, "No claims should succeed");
}
}
// Explanation of the POC test
The test deploys the Game contract with an initial claim fee of 0.1 ETH, a 1-day grace period, a 10% fee increase, and a 5% platform fee.
It logs the initial currentKing (address(0)) and claimFee (0 ETH due to a constructor issue).
Player A attempts to call claimThrone() with 0.1 ETH, but it reverts due to require(msg.sender == currentKing), as msg.sender (0x2) does not equal address(0).
Player B also attempts to claim and reverts for the same reason.
The test verifies that no king is set (currentKing = address(0)) and no claims succeed (totalClaims = 0), demonstrating that the game is unplayable.
// Output is as follows:
$ forge test --match-path test/GameTest.t.sol -vvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 2 tests for test/GameTest.t.sol:GameTest
[PASS] testClaimThroneBug() (gas: 101556)
Logs:
Initial currentKing: 0x0000000000000000000000000000000000000000
Initial claimFee: 0 ETH
Player A attempting to claim, currentKing: 0x0000000000000000000000000000000000000000
Player B attempting to claim, currentKing: 0x0000000000000000000000000000000000000000
[PASS] testInitialState() (gas: 25458)
Logs:
Initial claimFee: 0 ETH
Initial currentKing: 0x0000000000000000000000000000000000000000
Game ended: false
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.60ms (787.50µs CPU time)
Ran 1 test suite in 4.52ms (1.60ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Recommended Mitigation

- remove this code
require(msg.sender = currentKing, "Game: You are already the king. Cannot re-claim.");
+ add this code
require(msg.sender != currentKing, "Game: You are already the king. Cannot re-claim.");
Updates

Appeal created

inallhonesty Lead Judge 9 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.