Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Owner Can Call `updateGracePeriod` Mid-Round, Which Could Lead To Unfair Game Conditions

Owner Can Call updateGracePeriod Mid-Round, Which Could Lead To Unfair Game Conditions

Description

  • In a normal game round, when a player claims the throne, they become the current king, and a countdown (the gracePeriod) begins. If no one else claims within that period, the king can be declared the winner. This grace period is expected to remain fixed for that round to preserve fairness and predictability.

  • However, the contract allows the owner to call updateGracePeriod() at any time, including during an active round. This means the owner could shorten or extend the grace period after a player has already claimed the throne, potentially manipulating the timing of winner declaration in their own favor or to the detriment of players.

@> function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
gracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}

Risk

Likelihood:

  • This will occur when the contract owner calls updateGracePeriod() during an ongoing round in which a player is actively waiting out the grace period.

  • It may also occur just before a player is about to win, allowing the owner to end or delay the round arbitrarily.

Impact:

  • Players are denied a fair opportunity to win by having their grace period shortened or extended without notice.

  • The owner gains unfair influence over game outcomes, enabling potential manipulation or sabotage.

Proof of Concept

Add this test to Game.t.sol and run with forge test --mt testOwnerCanUpdateGracePeriodMidRound

function testOwnerCanUpdateGracePeriodMidRound() public {
// Player 1 claims throne and becomes King
vm.startPrank(player1);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
// 23 hours pass
vm.warp(block.timestamp + 23 hours);
// Player 2 claims throne and becomes King
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
// Owner updates grace period to 1 hour
vm.startPrank(deployer);
game.updateGracePeriod(1 hours);
vm.stopPrank();
// A little over 1 hour passes so grace period expires
vm.warp(block.timestamp + 1 hours + 1);
// Player 2 declares themself the winner even though they only had to wait 1 hour while Player 1 had to wait 23 hours
vm.startPrank(player2);
game.declareWinner();
vm.stopPrank();
assert(game.pendingWinnings(player2) > 0);
}

Recommended Mitigation

Declare a new variable futureGracePeriod :

// Game Parameters (Configurable by Owner)
uint256 public initialClaimFee; // The starting fee for a new game round
uint256 public feeIncreasePercentage; // Percentage by which the claimFee increases after each successful claim (e.g., 10 for 10%)
uint256 public platformFeePercentage; // Percentage of the claimFee that goes to the contract owner (deployer)
uint256 public initialGracePeriod; // The grace period set at the start of a new game round
+ uint256 public futureGracePeriod; // The grace period set in updateGracePeriod

Set futureGracePeriod to _newGracePeriod in updateGracePeriod:

function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
- gracePeriod = _newGracePeriod;
+ futureGracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}

Create an if statement in resetGame to set gracePeriod to either futureGracePeriod or initialGracePeriod:

function resetGame() external onlyOwner gameEndedOnly {
currentKing = address(0);
lastClaimTime = block.timestamp;
pot = 0;
claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
+ if (futureGracePeriod > 0) {
+ gracePeriod = futureGracePeriod;
+ }
gameEnded = false;
gameRound = gameRound + 1;
// totalClaims is cumulative across rounds, not reset here, but could be if desired.
emit GameReset(gameRound, block.timestamp);
}
Updates

Appeal created

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!