Root Cause: Missing Grace Period Check in claimThrone
-> Impact: Vulnerability to Eternal Game Front-Running
The Game
contract’s claimThrone()
function should have a check to prevent claims after the grace period expires.
Due to the lack of the check, as a result, malicious users can still call claimThrone()
well beyond the intended deadline and front-run the declareWinner()
function, resetting the game timer indefinitely. In practice, an attacker can repeatedly claim the throne after the grace period to prevent the game from ending, , effectively freezing the game and ensuring they eventually become the last claimant.
Likelihood: High
Missing Time Check: The claimThrone()
function does not verify whether the grace period has passed. Normally, once lastClaimTime + gracePeriod
is exceeded, no further claims should be allowed. However, no require(block.timestamp <= lastClaimTime + gracePeriod)
(or similar) is present, so anyone can call claimThrone()
at any time as long as gameEnded
is still false.
Front-Running declareWinner()
: Because claimThrone()
can still be called after the grace period, an attacker in the mempool can see a pending declareWinner()
transaction and race it. By sending their own claimThrone()
with a higher gas price, they reset lastClaimTime
and increase claimFee
, causing declareWinner()
to revert (“Grace period has not expired yet”). The next opportunity to end the game is pushed out by another full grace interval.
Interplay of State Variables: On each (malicious) claim, currentKing
is updated and lastClaimTime
is set to the new timestamp. Only after the grace period elapses without any claims can declareWinner()
succeed. Without the missing time-check, a well-capitalized attacker can continuously reset the timer. This mirrors other DoS-style exploits in “king of the hill” contracts: for example, in the famous KingOfEther contract, an attacker’s contract prevented any refunds, making themselves the eternal king and permanently DoSing the game.
Game Logic Flaw: The code comments even note the danger: // @info: allowed to claim throne even after grace period // @danger: declareWinner ... front-run
. In effect, the game can never naturally conclude if an attacker chooses to claim at the last moment. This breaks the intended game flow and locks the pot until the attacker is the last claimant.
Impact: Medium
This flaw has serious consequences: an attacker can permanently lock out all other players and claim the entire pot at will. In practice:
Denial of Service to Game End: By continuously resetting the timer, the attacker forces the game into a loop, effectively making it impossible for any honest player to win or for the game to conclude. This is a form of DoS on the game mechanics. A similar outcome occurred in the “KingOfEther” contract, where an attacker became the “eternal king” and prevented anyone else from dethroning them.
Fund Expropriation: The malicious player ultimately collects all of the accumulated pot (which can be very large). This can rival historic cases: for example, Fomo3D’s winner netted over 10,469 ETH (~$2.1M) by using high-gas strategies to block others. In this game, the attacker similarly ensures they are the final claimant, draining the pot into their account.
Trust and Fairness Eroded: Other players lose faith when the game never fairly ends or when only a few with deep pockets can manipulate it. Repeatedly inflated claimFee
values (due to fee increases on each claim) make participation prohibitively expensive, further centralizing power to attackers. Over time, this undermines the integrity and intended incentive structure of the game.
Platform Reputation and Funds at Risk: If deployed in a real DeFi context, such a vulnerability could lead to significant financial losses for users and damage the project’s credibility. Funds allocated as platform fees or player stakes could remain locked or be effectively stolen by the attacker.
Overall, this is a critical logic vulnerability: it combines a missing time check with the inherent front-running risk of public transactions. Without addressing it, the game can become an endless, unresolvable contest favoring the highest-bidding player.
step 1: go to test/Game.t.sol
file
step 2: paste the above code ⬆️
step 3: run the test suite
step 4: See the Output
Running this test (
forge test --mt
) reveals the issue: the call todeclareWinner()
reverts unless no one front-runs, and the last attacker (Player3) inevitably wins the game.
Scenario:
Initial Claim: Player1 calls claimThrone()
with the required INITIAL_CLAIM_FEE
. They become currentKing
and lastClaimTime
is set to now.
Subsequent Claim Before Grace: After just under one day (gracePeriod
) has passed, Player2 calls claimThrone()
(paying the incremented fee) and becomes the new king. lastClaimTime
updates.
Grace Period Expires: Time advances beyond lastClaimTime + gracePeriod
, so normally Player2 could call declareWinner()
to end the game. But Player3 sees this pending situation on-chain.
Front-Run Claim: Player3 immediately sends a transaction calling claimThrone()
(with the new higher fee) and outbids the miner fee. This resets lastClaimTime
to the current time (and increments the fee again).
DeclareWinner Reverts: Now, if anyone (even Player3) calls declareWinner()
, it reverts because block.timestamp
is no longer > lastClaimTime + gracePeriod
. In effect, the game’s end has been postponed by another day.
Game Eventually Ends: If Player3 chooses not to front-run again, after another full day passes, calling declareWinner()
will succeed. The contract will award the entire pot to Player3 (as they are currentKing
).
This shows an attacker can time and fund transactions to ensure they are the last claimant, effectively denying anyone else the win and monopolizing the pot.
Tools Used:
Foundry Test Suite
Chat-GPT AI Assistance (Report Grammar Check & Improvements)
Manual Review
To remediate and harden the contract:
Enforce Grace Period in claimThrone()
: Add a time check at the start of claimThrone()
, for example:
This ensures no one can claim after the deadline, so once the grace period passes, only declareWinner()
can be called.
Guard declareWinner()
Access: Maintain the existing require in declareWinner()
(block.timestamp > lastClaimTime + gracePeriod
), but consider adding an additional safeguard or state flag once it succeeds so claims are disabled (though gameEnded
already prevents further claims).
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.