Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

Missing Payout to Previous King Breaks Incentive mechanism which violates the game policy and may discourage users from participating in the game.

Root + Impact

Description

The claimThrone() function in the contract is expected to follow the game rule:

"Receives a small payout from the next player's claimFee (if applicable)."

This implies that the dethroned (previous) king should be rewarded with a portion of the ETH sent by the next claimant.

However, in the current implementation, this rule is entirely unimplemented:

  • The previousKingPayout variable is declared but never assigned a value and no ETH is sent to the previous currentKing before a new king is declared.

uint256 previousKingPayout = 0;
https://github.com/CodeHawks-Contests/2025-07-last-man-standing/blob/47d9d19a78acb52270269f4bff1568b87eb81a96/src/Game.sol#L191

Risk

Likelihood:

  • It occurs anytime the throne is claimed

Impact:

  • The Game's policy is violated, which breach users trust.

  • lack of incentives for dethroned players might discourage them from interacting with the protocol.

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();
}
function testPreviousKingNotIncentiviced() public {
uint256 player1BalanceBefore = player1.balance; // player 1 balance before claiming throne
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();// player1 claim throne
vm.startPrank(player2);
game.claimThrone{value: game.claimFee()}(); // New player claim throne
vm.stopPrank();
uint256 player1BalanceAfter = player1.balance;
assertEq(player1BalanceAfter, player1BalanceBefore - INITIAL_CLAIM_FEE);
// player1 balance does not change after player2 claims throne
}
}

Recommended Mitigation

  • Allocate a percentage of the Game::claimFee to the Game::currentKing before a new Game::currentKing is declared.

+ uint256 previousKingPayout = (sentAmount * previousKingRewardPercentage) / 100;
.
.
.
+ pendingWinnings[currentKing] += previousKingPayout;
.
.
.
currentKing = msg.sender;
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Missing Previous King Payout Functionality

Support

FAQs

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