Last Man Standing

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

Owner Can Manipulate Grace Period to Prematurely End Rounds

Root + Impact

Description

  • In normal operation, the game requires that the owner "cannot declare a winner before the grace period expires," and "cannot reset the game if a round is still active."

  • However, the owner can exploit the updateGracePeriod function during an active round to artificially shorten the grace period (to something like 1 second). This allows the owner to immediately declare a winner and reset the game, bypassing the intended game mechanics.

function declareWinner() external gameNotEnded {
require(currentKing != address(0), "Game: No one has claimed the throne yet.");
require(
@> block.timestamp > lastClaimTime + gracePeriod,
"Game: Grace period has not expired yet."
);
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
pot = 0; // Reset pot after assigning to winner's pending winnings
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}
@> 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:

  • The owner can execute this exploit at any time during an active round

  • The attack requires minimal steps: one transaction to update the grace period and another to declare winner

Impact:

  • Violates game fairness and destroys player trust in the system

  • Enables rug pull scenarios where the owner colludes with the current king (or is the current king themselves)

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console2, console} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameTest is Test {
Game public game;
address public deployer;
address public player1;
address public player2;
address public player3;
// Initial game parameters for testing
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether; // 0.1 ETH
uint256 public constant GRACE_PERIOD = 1 days; // 1 day in seconds
uint256 public constant FEE_INCREASE_PERCENTAGE = 10; // 10%
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5; // 5%
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
player3 = makeAddr("player3");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.deal(player3, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
function test_ownerDeclareWinner() public {
vm.prank(player1);
game.claimThrone{value: 0.1 ether}();
vm.prank(player2);
game.claimThrone{value: 0.2 ether}();
vm.prank(player3);
game.claimThrone{value: 0.3 ether}();
// normal behavior: owner can't reset game or declare winner
vm.prank(deployer);
vm.expectRevert("Game: Game has not ended yet.");
game.resetGame();
// exploit
vm.startPrank(deployer);
game.updateGracePeriod(1 seconds);
skip(3 seconds); // wait a few seconds
game.declareWinner();
game.resetGame(); // owner can now reset game and declare winner
vm.stopPrank();
}
}

Recommended Mitigation

Make updateGracePeriod allowed only after the game ends.

- function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
+ function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner gameEndedOnly {
Updates

Appeal created

inallhonesty Lead Judge 10 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
kjcao Submitter
10 days ago
inallhonesty Lead Judge
9 days ago
kjcao Submitter
9 days ago
inallhonesty Lead Judge
7 days ago
inallhonesty Lead Judge 6 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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