Last Man Standing

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

Throne Can Be Claimed After Grace Period Ends

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:

  1. 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.

  2. 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.

//@audit-issue Front-running
function claimThrone() external payable gameNotEnded nonReentrant {
//@audit-issue allowing malicious actor to claim the throne right after grace period expires and before decalare winner function be call
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.");
// ... another logic
}

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 {
// Checks how long the grace period is
console2.log("\n Grace period is: %s", game.gracePeriod());
// 1. Player1 will become the winner
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();
// 2. get the remaining time till grace period ended
uint256 getTheRemainingTimeAfterPlayer1Claim = game.getRemainingTime();
console2.log("\n Time remaining until the grace period expires: %s", getTheRemainingTimeAfterPlayer1Claim);
// 3. Simulate the grace period has ended
vm.warp(block.timestamp + game.gracePeriod() + 1);
// 4. get the remaining time after grace period end
uint256 getTheRemainingTimeAfterGracePeriodEnd = game.getRemainingTime();
console2.log("\n Time remaining when the grace period was expires: %s", getTheRemainingTimeAfterGracePeriodEnd);
// 5. Malicious actor Claim the Throne right in time when grace period has ended and before anyone call declare winner
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();
// 6. get the remaining time after malicious actor claim the throne
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
}
Updates

Appeal created

inallhonesty Lead Judge about 1 month 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.