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 {
vm.startPrank(player1);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
vm.warp(block.timestamp + 23 hours);
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}();
vm.stopPrank();
vm.startPrank(deployer);
game.updateGracePeriod(1 hours);
vm.stopPrank();
vm.warp(block.timestamp + 1 hours + 1);
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);
}