Last Man Standing

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

`Game::owner()` can upadate the Grace period with `Game::updateGracePeriod(uint256 _newGracePeriod)` anytime and manipulate game outcome in their favor

Root + Impact

Description

The Game::updateGracePeriod(uint256 _newGracePeriod) function allows the contract owner to arbitrarily change the gracePeriod at any time—even while a game is actively ongoing. Since the grace period determines when a winner can be declared, this function introduces a centralized attack vector where Game::owner() can manipulate the game outcome in their favor by changing the grace period to suit their purpose

https://github.com/CodeHawks-Contests/2025-07-last-man-standing/blob/47d9d19a78acb52270269f4bff1568b87eb81a96/src/Game.sol#L283

Risk

Likelihood:

  • Owner can change grace period anytime

Impact:

If the owner participates in the game, they can:

  • Claim the throne (become currentKing)

  • Reduce the grace period to near-zero

  • Immediately call Game::declareWinner() to declare themselves the winner, and gain all the eth in the pot.

This enables the owner to force-win the game

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, 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;
address public maliciousActor;
// 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");
maliciousActor = makeAddr("maliciousActor");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.deal(player3, 10 ether);
vm.deal(maliciousActor, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
modifier loadGame() {
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}(); // Player1 claim throne
console.log(game.currentKing());
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}(); // Player2 claim throne
vm.stopPrank();
_;
}
function testOwnerCanUpdateGracePeriodAnytime() public loadGame {
vm.warp(GRACE_PERIOD - 4 hours); // warp time to simulate an active game
// indicating 20hrs passed since last throne claim
vm.startPrank(game.owner());
game.claimThrone{value: game.claimFee()}(); // Owner claims throne
game.updateGracePeriod(1); // owner updates gracePeriod to 1 second
vm.warp(block.timestamp + 2); // simulate time passage of 2 seconds
vm.stopPrank();
vm.expectEmit(true, false, false, false, address(game));
emit GameEnded(
game.owner(),
game.pot(),
block.timestamp,
game.gameRound()
); // emit an avaent to mark the end of the game
vm.prank(player2);
game.declareWinner();
}
}

Recommended Mitigation

  • Only allow Game::updateGracePeriod to be called when there is no active game by adding gameEndedOnly modifier.

- function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner{...}
+ function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner gameEndedOnly{...}
  • Restrict Game::owner() so that they can only set initial game parameters before new round starts. Eg Game::owner() can set initialGracePeriod not gracePeriod

function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(
_newGracePeriod > 0,
"Game: New grace period must be greater than zero."
);
- gracePeriod = _newGracePeriod;
+ initialGracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}
  • Restrict Game::owner() from participating in game by adding the below checker

    function claimThrone() external payable gameNotEnded nonReentrant{
    .
    .
    .
    require(msg.sender != owner(), "Owner cannot participate");
    .
    .
    .
    .
    }
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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