Last Man Standing

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

Broken game logic due to lack of enforcement after grace period.


Logic flaw in Game::claimThrone, allows new player to overwrite current king after grace period, violating NetSpec of the Contract

Description

  • Under normal behavior, the player who last claimed the throne becomes the currentKing. Once the grace period (e.g., 86400 seconds) has passed since the last claim, the currentKing is expected to be declared the winner by calling declareWinner(). No new claims should be allowed after the grace period expires, ensuring fairness and consistency with the NetSpec.

  • The claimThrone() function remains callable even after the grace period expires, allowing a malicious actor to overwrite the currentKing just before declareWinner() is called. This breaks the intended logic where the king at the end of the grace period should win, leading to a functional flaw and potential unfair outcomes.

// Root cause in the codebase with @> marks to highlight the relevant section

RISK

Likelihood:

  • This issue occurs when a new user claims the throne immediately after the grace period has ended, but before the declareWinner() function is called. The grace period resets on every new claim, allowing a malicious actor to overtake the rightful winner.

  • The window for exploitation is consistently available after every grace period, making the bug reliably reproducible in any round of the game.

Impact:

  • The legitimate winner (i.e., the player holding the throne at the end of the grace period) can be bypassed, violating the intended game logic and fairness.

  • A malicious actor can continuously reset the lastClaimTime by claiming the throne right before the grace period ends, effectively preventing the round from ever ending and blocking the declareWinner logic indefinitely — resulting in a functional denial of service.

Proof of Concept

Note: This PoC assumes the following modification in the Game::claimThrone function:

// Original

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

// Modified to reveal/fix the logic flaw

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


Place the following into the Game.t.sol

function test_MaliciousActorDeclarewinner() public {
vm.deal(address(game), 120 ether);
vm.deal(player1, 9 ether);
vm.deal(maliciousActor, 10 ether);
vm.prank(player1);
game.claimThrone{value: 5 ether}();
assertEq(game.currentKing(), player1);
vm.warp(GRACE_PERIOD + 1);
vm.prank(maliciousActor);
game.claimThrone{value: 10 ether}();
assertEq(game.currentKing(), maliciousActor);
vm.expectRevert("Game: Grace period has not expired yet.");
vm.prank(maliciousActor);
game.declareWinner();
}

Recommended Mitigation

Implement a check within the claimThrone function to prevent new claims after the grace period has expired, ensuring the current king at that time is declared the winner before a new round can begin.

+ require(
+ block.timestamp <= lastClaimTime + gracePeriod,
+ "Game: Grace period has expired. Declare a winner."
+ );
Updates

Appeal created

inallhonesty Lead Judge 15 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Game::claimThrone can still be called regardless of the grace period

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.