Throne Can Be Claimed After Grace Period Ends
Description
The claimThrone()
function does not validate whether the grace period has ended before allowing users to claim the throne. This design flaw leads to two significant issues:
Late Claims after Grace Period: Players can continue to call claimThrone()
even after the game should have ended, which resets the timer (lastClaimTime
) and extends the game unfairly.
Front-running Vulnerability: Without a cutoff time, attackers can submit a transaction at the very end of the grace period (or front-run a visible transaction in the mempool) to become the last claimant and secure the throne unfairly.
Both issues undermine the fairness and finality of the game logic.
function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
}
Risk
Likelihood:
-
Any user (or bot) can exploit this at will.
-
Especially dangerous in a competitive or incentivized game (e.g., winner receives the pot).
-
Mempool visibility allows opportunistic front-running.
Impact:
-
The game can be prolonged indefinitely, blocking finalization and pot distribution.
-
Unfair final kingship may be awarded to a front-runner, reducing game integrity.
-
Undermines trust in fairness of winner selection.
Proof of Concept
here's the test that shown malicious actor can Claim the Throne after the grace period has ended and reseting the grace period:
function test_StillManageToClaimThroneAfterGracePeriodEnd() public {
console2.log("\n Grace period is: %s", game.gracePeriod());
console2.log("\n Player1 Claim the Throne");
vm.startPrank(player1);
game.claimThrone{value: game.claimFee()}();
console2.log("\n Current King is: %s", game.currentKing());
vm.stopPrank();
uint256 getTheRemainingTimeAfterPlayer1Claim = game.getRemainingTime();
console2.log("\n Time remaining until the grace period expires: %s", getTheRemainingTimeAfterPlayer1Claim);
vm.warp(block.timestamp + game.gracePeriod() + 1);
uint256 getTheRemainingTimeAfterGracePeriodEnd = game.getRemainingTime();
console2.log("\n Time remaining when the grace period was expires: %s", getTheRemainingTimeAfterGracePeriodEnd);
console2.log("\n Malicious actor Claim the Throne before anyone call declare winner");
vm.startPrank(maliciousActor);
game.claimThrone{value: game.claimFee()}();
console2.log("\n Current King is: %s", game.currentKing());
vm.stopPrank();
uint256 getTheRemainingTimeAfterMaliciousActorClaim = game.getRemainingTime();
assertEq(getTheRemainingTimeAfterMaliciousActorClaim, 1 days);
console2.log("\n Time remaining after Malicious Actor Claim the Throne: ", getTheRemainingTimeAfterMaliciousActorClaim);
}
and the log of the test:
Ran 1 test for test/Game.t.sol:GameTest
[PASS] test_StillManageToClaimThroneAfterGracePeriodEnd() (gas: 219707)
Logs:
Grace period is: 86400
Player1 Claim the Throne
Current King is: 0x7026B763CBE7d4E72049EA67E89326432a50ef84
Time remaining until the grace period expires: 86400
Time remaining when the grace period was expires: 0
Malicious actor Claim the Throne before anyone call declare winner
Current King is: 0x195Ef46F233F37FF15b37c022c293753Dc04A8C3
Time remaining after Malicious Actor Claim the Throne 86400
Recommended Mitigation
Introduce a new dynamic variable gameDeadline
to track the end of the grace period. Do not initialize the game timer (lastClaimTime
or gameDeadline
) in the constructor. Instead, start the game only upon the first claim.
This approach ensures:
-
The game does not start running until there is actual user activity.
-
Prevents the game from expiring before any participation.
-
Supports a fair and competitive dynamic grace period.
+ uint256 public gameDeadline; // new dynamic variable
+ uint256 public constant EXTENSION_WINDOW = 5 minutes; // new variable
+ uint256 public constant EXTENSION_DURATION = 5 minutes; // new variable
function claimThrone() external payable gameNotEnded nonReentrant {
// Start the game on first claim
+ if (gameDeadline == 0) {
+ lastClaimTime = block.timestamp;
+ gameDeadline = block.timestamp + GRACE_PERIOD;
+ }
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
// Reject claims if grace period has ended
+ require(
+ block.timestamp < gameDeadline,
+ "Game: Grace period has ended. No more claims allowed."
+ );
// ... another logic
// Extend the game if claim is made near the end
+ if (block.timestamp + EXTENSION_WINDOW >= gameDeadline) {
+ gameDeadline += EXTENSION_DURATION;
+ emit GameExtended(gameDeadline);
+ }
// ... another logic
}