Last Man Standing

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

Owner Can Manipulate All Critical Game Parameters Mid-Round, Undermining Game Fairness

Description

The game's integrity relies on a stable and predictable set of rules for the duration of an active round. Players commit funds and strategize based on the currently defined gracePeriod and fee structure.

However, all administrative functions—updateGracePeriod(), updateClaimFeeParameters(), and updatePlatformFeePercentage()—can be called by the owner at any time, including during an active game round. This allows the owner to alter the fundamental rules of the game while players' funds are already committed to the pot. A malicious or careless owner can change these parameters to unfairly influence the game's outcome, for example, by drastically shortening the gracePeriod to force a quick win for the current king, or by increasing the claimFee to an impossibly high value to prevent competitors.

// src/Game.sol:307-311
function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
@> gracePeriod = _newGracePeriod; // No check to see if a game is active
emit GracePeriodUpdated(_newGracePeriod);
}
// NOTE: The same lack of protection exists in updateClaimFeeParameters() and updatePlatformFeePercentage().

Risk

Likelihood: Medium

  • The action can be performed at any time by a single actor (the owner). The lack of any safeguards makes it a plausible scenario, whether the intent is malicious or accidental.

Impact: High

  • Game Fairness Compromised: The owner can directly manipulate the game's outcome. For example, they could shorten the gracePeriod to guarantee a win for a specific player, or raise the feeIncreasePercentage to 100% to make the game prohibitively expensive for new players.

  • Erosion of Trust: Players cannot trust that the rules they started playing by will be the same rules at the end of the round. This fundamentally breaks the competitive integrity of a "King of the Hill" game.

  • Potential for Collusion: This opens a direct vector for collusion between the owner and a specific player to secure the pot unfairly.

Proof of Concept

The following comprehensive test suite demonstrates that the owner can modify all critical game parameters during an active round. The tests prove that the owner can shorten the grace period to force an early win, and can also alter both the platform fees and the claim fee structure while the game is live.

Note: The PoC uses vm.store to set up an active game state, bypassing a separate critical bug in claimThrone().

Test File: test/OwnerManipulationVerification.t.sol

// test/OwnerManipulationVerification.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {Game} from "../src/Game.sol";
/**
* @title OwnerManipulationVerificationTest
* @dev Verify the accuracy of "Owner Can Manipulate Game Parameters" report
* @dev Tests whether owner can actually change game parameters during active round
*/
contract OwnerManipulationVerificationTest is Test {
Game public game;
address public owner;
address public player1;
address public player2;
function setUp() public {
owner = makeAddr("owner");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
vm.deal(owner, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.startPrank(owner);
// Create new game with 1 day grace period
game = new Game(0.1 ether, 1 days, 10, 5);
vm.stopPrank();
}
/**
* @dev Test verifying that owner can change gracePeriod during active game
* @dev This simulates the scenario described in the report
*/
function test_OwnerCanManipulateGracePeriodDuringActiveGame() public {
console2.log("=== Testing Owner Manipulation of Grace Period During Active Game ===");
// --- Setup: Simulate player becoming king ---
// Need to bypass logical error in claimThrone using vm.store
bytes32 kingSlot = bytes32(uint256(1));
bytes32 lastClaimTimeSlot = bytes32(uint256(2));
bytes32 potSlot = bytes32(uint256(4));
// Set player1 as current king
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
// Set last claim time
vm.store(address(game), lastClaimTimeSlot, bytes32(block.timestamp));
// Set amount in pot
vm.store(address(game), potSlot, bytes32(uint256(1 ether)));
// Verify setup
assertEq(game.currentKing(), player1, "Setup failed: King was not set correctly");
assertEq(game.pot(), 1 ether, "Setup failed: Pot was not set correctly");
uint256 originalGracePeriod = game.gracePeriod();
console2.log("Original grace period:", originalGracePeriod, "seconds");
assertEq(originalGracePeriod, 1 days, "Original grace period should be one day");
// --- Scenario: Owner manipulates the game ---
// Advance time to just before original grace period expires
vm.warp(block.timestamp + originalGracePeriod - 30 minutes);
console2.log("Current time:", block.timestamp);
console2.log("Original grace period expiry time:", game.lastClaimTime() + originalGracePeriod);
// At this point, winner cannot be declared yet
vm.expectRevert("Game: Grace period has not expired yet.");
game.declareWinner();
console2.log("Confirmed: Cannot declare winner before grace period expires");
// Owner drastically shortens the grace period
vm.startPrank(owner);
uint256 newGracePeriod = 1; // Only 1 second!
game.updateGracePeriod(newGracePeriod);
vm.stopPrank();
console2.log("New grace period:", game.gracePeriod(), "seconds");
assertEq(game.gracePeriod(), newGracePeriod, "Failed to update grace period");
// --- Verify result ---
// Now winner can be declared because new grace period has expired
// block.timestamp > lastClaimTime + newGracePeriod
uint256 timeElapsed = block.timestamp - game.lastClaimTime();
console2.log("Time elapsed since last claim:", timeElapsed, "seconds");
console2.log("New grace period:", newGracePeriod, "seconds");
assertTrue(timeElapsed > newGracePeriod, "Time elapsed should be greater than new grace period");
// Now winner can be declared
game.declareWinner();
assertTrue(game.gameEnded(), "VULNERABILITY CONFIRMED: Owner was able to force early game end");
console2.log("VULNERABILITY CONFIRMED: Owner was able to change game rules during active round");
}
/**
* @dev Test verifying that owner can change platform fees during active game
*/
function test_OwnerCanManipulatePlatformFeesDuringActiveGame() public {
console2.log("=== Testing Owner Manipulation of Platform Fees During Active Game ===");
// Setup active game
bytes32 kingSlot = bytes32(uint256(1));
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
uint256 originalPlatformFee = game.platformFeePercentage();
console2.log("Original platform fee:", originalPlatformFee, "%");
// Owner changes platform fees during active game
vm.startPrank(owner);
uint256 newPlatformFee = 50; // 50% instead of 5%
game.updatePlatformFeePercentage(newPlatformFee);
vm.stopPrank();
assertEq(game.platformFeePercentage(), newPlatformFee, "Failed to update platform fee");
console2.log("New platform fee:", game.platformFeePercentage(), "%");
console2.log("VULNERABILITY CONFIRMED: Owner was able to change platform fees during active game");
}
/**
* @dev Test verifying that owner can change claim fee parameters during active game
*/
function test_OwnerCanManipulateClaimFeeParametersDuringActiveGame() public {
console2.log("=== Testing Owner Manipulation of Claim Fee Parameters During Active Game ===");
// Setup active game
bytes32 kingSlot = bytes32(uint256(1));
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
uint256 originalInitialClaimFee = game.initialClaimFee();
uint256 originalFeeIncreasePercentage = game.feeIncreasePercentage();
console2.log("Original initial claim fee:", originalInitialClaimFee);
console2.log("Original fee increase percentage:", originalFeeIncreasePercentage, "%");
// Owner changes fee parameters during active game
vm.startPrank(owner);
uint256 newInitialClaimFee = 10 ether; // Huge increase
uint256 newFeeIncreasePercentage = 100; // Double fees each time
game.updateClaimFeeParameters(newInitialClaimFee, newFeeIncreasePercentage);
vm.stopPrank();
assertEq(game.initialClaimFee(), newInitialClaimFee, "Failed to update initial claim fee");
assertEq(game.feeIncreasePercentage(), newFeeIncreasePercentage, "Failed to update fee increase percentage");
console2.log("New initial claim fee:", game.initialClaimFee());
console2.log("New fee increase percentage:", game.feeIncreasePercentage(), "%");
console2.log("VULNERABILITY CONFIRMED: Owner was able to change fee parameters during active game");
}
/**
* @dev Test to verify no protection mechanisms exist
*/
function test_NoProtectionMechanismsExist() public {
console2.log("=== Testing Absence of Protection Mechanisms ===");
// Verify that admin functions don't contain gameEndedOnly modifier
// This is done by trying to call functions during active game
// Setup active game
bytes32 kingSlot = bytes32(uint256(1));
vm.store(address(game), kingSlot, bytes32(uint256(uint160(player1))));
// Verify game is not ended
assertFalse(game.gameEnded(), "Game should be active");
vm.startPrank(owner);
// These calls should succeed because there are no protection mechanisms
game.updateGracePeriod(2 days);
game.updatePlatformFeePercentage(20);
game.updateClaimFeeParameters(0.2 ether, 15);
vm.stopPrank();
console2.log("Confirmed: All admin functions work during active game");
console2.log("NO PROTECTION MECHANISMS exist to prevent parameter changes during active game");
}
}

Successful Test Output:

$ forge test --match-path test/OwnerManipulationVerification.t.sol -vv
Ran 4 tests for test/OwnerManipulationVerification.t.sol:OwnerManipulationVerificationTest
[PASS] test_NoProtectionMechanismsExist() (gas: 51036)
Logs:
=== Testing Absence of Protection Mechanisms ===
Confirmed: All admin functions work during active game
NO PROTECTION MECHANISMS exist to prevent parameter changes during active game
[PASS] test_OwnerCanManipulateClaimFeeParametersDuringActiveGame() (gas: 43859)
Logs:
=== Testing Owner Manipulation of Claim Fee Parameters During Active Game ===
Original initial claim fee: 100000000000000000
Original fee increase percentage: 10 %
New initial claim fee: 10000000000000000000
New fee increase percentage: 100 %
VULNERABILITY CONFIRMED: Owner was able to change fee parameters during active game
[PASS] test_OwnerCanManipulateGracePeriodDuringActiveGame() (gas: 87518)
Logs:
=== Testing Owner Manipulation of Grace Period During Active Game ===
Original grace period: 86400 seconds
Current time: 84601
Original grace period expiry time: 86401
Confirmed: Cannot declare winner before grace period expires
New grace period: 1 seconds
Time elapsed since last claim: 84600 seconds
New grace period: 1 seconds
VULNERABILITY CONFIRMED: Owner was able to change game rules during active round
[PASS] test_OwnerCanManipulatePlatformFeesDuringActiveGame() (gas: 31952)
Logs:
=== Testing Owner Manipulation of Platform Fees During Active Game ===
Original platform fee: 5 %
New platform fee: 50 %
VULNERABILITY CONFIRMED: Owner was able to change platform fees during active game
Suite result: ok. 4 passed; 0 failed; 0 skipped

The successful execution of the entire test suite confirms that all administrative parameter-setting functions lack the necessary safeguards and can be called during an active game, validating the vulnerability.

Recommended Mitigation

To preserve the integrity and fairness of the game, all administrative functions that alter core game mechanics should be disabled while a game round is active. These parameters should only be configurable between rounds.

The most effective way to enforce this is to add the gameEndedOnly modifier to all parameter-setting functions.

// src/Game.sol:307
- function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner {
+ function updateGracePeriod(uint256 _newGracePeriod) external onlyOwner gameEndedOnly {
require(_newGracePeriod > 0, "Game: New grace period must be greater than zero.");
gracePeriod = _newGracePeriod;
emit GracePeriodUpdated(_newGracePeriod);
}
// src/Game.sol:317
function updateClaimFeeParameters(
uint256 _newInitialClaimFee,
uint256 _newFeeIncreasePercentage
- ) external onlyOwner isValidPercentage(_newFeeIncreasePercentage) {
+ ) external onlyOwner gameEndedOnly isValidPercentage(_newFeeIncreasePercentage) {
require(_newInitialClaimFee > 0, "Game: New initial claim fee must be greater than zero.");
initialClaimFee = _newInitialClaimFee;
feeIncreasePercentage = _newFeeIncreasePercentage;
emit ClaimFeeParametersUpdated(_newInitialClaimFee, _newFeeIncreasePercentage);
}
// src/Game.sol:329
function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage)
external
onlyOwner
+ gameEndedOnly
isValidPercentage(_newPlatformFeePercentage)
{
platformFeePercentage = _newPlatformFeePercentage;
emit PlatformFeePercentageUpdated(_newPlatformFeePercentage);
}

This change ensures that the rules of the game are locked in once a round begins, providing a fair and predictable environment for all players.

Updates

Appeal created

inallhonesty Lead Judge 10 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.