Race Condition Allows "Winning" King to be Dethroned After Grace Period Expires
Normal Behavior: The core premise of the game is that if a king remains on the throne for the entire gracePeriod, they are the winner. It is expected that once block.timestamp is greater than lastClaimTime + gracePeriod, the current king's victory is secured and they are entitled to the prize pot.
The Issue: The contract does not automatically lock the "winner" status when the grace period expires. The game continues and the claimThrone() function remains active until someone successfully calls declareWinner(). This creates a critical time window between the moment the grace period expires and the moment the declareWinner transaction is mined. An attacker can exploit this window by submitting a claimThrone transaction with a higher gas fee to front-run any pending declareWinner transactions. This allows the attacker to steal the throne from the rightful winner, resetting the timer and denying them their prize.
Likelihood: MEDIUM
This scenario is not just a theoretical edge case; it's a practical exploit. In any active game, there will be a delay (of at least one block) between when the grace period expires and when declareWinner is called and mined. An attacker can easily monitor the contract's state for this condition and execute a front-running attack. The only prerequisite is the financial incentive (the pot being large enough) to justify the gas cost.
Impact: HIGH
For the player who legitimately met the winning conditions, the impact is the total loss of the prize pot they had earned. This exploit undermines the fundamental fairness and integrity of the game. If this becomes known, it will destroy player trust and render the game unplayable, as there is no guaranteed path to victory.
Player A claims the throne. The lastClaimTime is set, and the gracePeriod is 24 hours. The pot is now substantial.
More than 24 hours pass. The grace period for Player A has now officially expired. Player A is the rightful winner.
A well-intentioned user, Player C, sees that the period has expired and submits a transaction to call declareWinner(). This transaction enters the public mempool with a standard gas fee.
An attacker, Player B, is monitoring the mempool. They see Player C's pending declareWinner transaction.
Player B immediately crafts and submits a transaction to call claimThrone(), but sets a much higher gas fee than Player C.
Observe: Due to the higher gas fee (priority fee), miners are incentivized to include Player B's claimThrone transaction in the next block before Player C's transaction.
Result: Player B's transaction executes first. It succeeds because gameEnded is still false. Player B becomes the new king, and lastClaimTime is reset to the current block.timestamp. Player A's win is nullified.
When Player C's declareWinner transaction is eventually mined, it reverts because the condition block.timestamp > lastClaimTime + gracePeriod is no longer true. The rightful winner, Player A, gets nothing.
Add this code to the test file:
The contract logic must be modified to "lock" the game state once a winner's grace period has expired, preventing any new claims.
Add a require check at the beginning of the claimThrone function to ensure it cannot be called if the current king is eligible to be declared the winner.
By adding this single check, the claimThrone function becomes automatically disabled the moment the grace period expires. From that point on, the only meaningful function that can be called is declareWinner. This completely closes the race condition window and guarantees that a king who survives the grace period cannot be unfairly dethroned.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.