Last Man Standing

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

Front-running of claimThrone() Blocks declareWinner()

Root + Impact

Description

  • Normal behavior: The claimThrone() function allows users to become the new king by sending a required claimFee, and resets the gracePeriod. Once the gracePeriod expires without a new claim, anyone can call declareWinner() to end the round and allocate the pot to the last king.

  • Issue: A malicious actor or bot can front-run a pending declareWinner() transaction by calling claimThrone() with a higher gas price. This resets the gracePeriod, preventing the winner declaration and potentially allowing the attacker to maintain control of the game indefinitely or until it is most profitable for them.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimThrone() external payable nonReentrant gameNotEnded {
require(msg.sender != currentKing, "Game: You are already the king.");
require(msg.value >= claimFee, "Game: Insufficient claim fee.");
@> lastClaimTime = block.timestamp; // resets the grace period on every claim
...
}
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;
...
}


Risk

Likelihood:

  • This occurs when the gracePeriod is about to expire and users are incentivized to declare a winner.

  • It is likely in adversarial environments or when bots monitor the mempool for profitable transactions (e.g., high pot size).

Impact:

  • Attackers can grief the system, prolonging the game indefinitely and preventing winners from being declared.

  • Users are discouraged from participating due to unfair behavior, harming trust in the platform.

Proof of Concept

// Honest user tries to declare winner
declareWinner() // tx seen in mempool
// Malicious actor monitors mempool and front-runs
claimThrone() // executes before declareWinner()
// Result: gracePeriod resets, declareWinner reverts

Recommended Mitigation

- lastClaimTime = block.timestamp;
+ require(block.timestamp < lastClaimTime + gracePeriod - lockBuffer, "Game: Too close to grace period end.");
+ lastClaimTime = block.timestamp;

You may define lockBuffer as a short duration (e.g., 10 seconds) during which claimThrone() cannot be called at the end of the grace period, ensuring fair execution of declareWinner().

Alternatively, you could adopt a commit-reveal system or minimum block gap enforcement between last claim and winner declaration to neutralize front-running opportunities.

Updates

Appeal created

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