Last Man Standing

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

Owner Can Extract 100% of Throne Claim Fees via Platform Fee calculation Manipulation

Root + Impact

Description

The Game::updatePlatformFeePercentage() function allows the contract owner to set the platformFeePercentage at any time whether a game is active or not, up to 100%. Combined with the fee calculation logic in Game::claimThrone(), this creates a vulnerability:

  • When a player claims the throne, the platformFeePercentage is applied to the full msg.value to get the currentPlatformFee.

  • There is a defensive check to ensure the platformFee doesn’t exceed the difference between sentAmount and previousKingPayout, but previousKingPayout is always 0, therefore currentPlatformFee == sentAmount

  • This means that Game::owner() can collect 100% of the ETH sent, and nothing goes to the pot.

This creates an honey pot, where all funds go to the owner.

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

Risk

Likelihood:

  • Reason 1: owner can update the platformFeeParameter at anytime during the game;

Impact:

  • Owner can continuously extract 100% of every throne claim fee.

  • Game becomes a honeypot: participants think they're contributing to the pot, but all funds go to the owner.

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 testOwnerCanSteal() public {
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}(); // player1 claims throne
vm.warp(GRACE_PERIOD / 2); // simulate time passage in an active game
uint256 platformFeesBalanceBefore = game.platformFeesBalance();// get current platFormFeesBalance
uint256 _newPercentage = 100; // 100% platform fee
vm.startPrank(game.owner());
game.updatePlatformFeePercentage(_newPercentage);// increase platform fee balance to 100%
vm.stopPrank();
uint256 claimFee = game.claimFee();
vm.startPrank(player2);
game.claimThrone{value: claimFee}();// claim fee goes to the owner as currentPlatformFee
vm.stopPrank();
uint256 platformFeesBalanceAfter = game.platformFeesBalance();
assertEq(
platformFeesBalanceAfter,
platformFeesBalanceBefore + claimFee
);
}
}

Recommended Mitigation

  • Restrict Game::updatePlatformFeePercentage() such that it can only be called when a game is not active/before the start of a new game. Eg by adding gameEndedOnly modifier.

- function updatePlatformFeePercentage(
- uint256 _newPlatformFeePercentage
- ) external onlyOwner isValidPercentage(_newPlatformFeePercentage){...}
+ function updatePlatformFeePercentage(
+ uint256 _newPlatformFeePercentage
+ ) external onlyOwner isValidPercentage(_newPlatformFeePercentage) gameEndedOnly{..}
  • or by adding a check to see if the grace period has successfully elapsed

function updatePlatformFeePercentage(
uint256 _newPlatformFeePercentage
) external onlyOwner isValidPercentage(_newPlatformFeePercentage) gameEndedOnly{
.
.
+ require(block.timestamp > lastClaimTime + gracePeriod );
}
  • Set the Game::platformFeePercentage within reasonable limits, e.g (0-20%) so that the check below in Game::claimThrone() is not triggered.

if (currentPlatformFee > (sentAmount - previousKingPayout)) {
currentPlatformFee = sentAmount - previousKingPayout;
}
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.