Last Man Standing

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

Spec deviation: previous king never receives payout

Precondition: Reachable once this Critical [Unreachable gameplay: wrong equality check bricks claimThrone()] (https://codehawks.cyfrin.io/c/2025-07-last-man-standing/s/cmdwlle4k0003l404vznah12s) is fixed (==!=).
PoC method: Tests use a minimal harness (GamePatched.sol) with only that one-line fix to reach the intended game state.

Root + Impact

Description

  • Expected behavior: The README (ACTORS → KING) and the claimThrone() docstring state the previous king receives a small payout from the next player’s claimFee.

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

  2. Docstring: If there's a previous king, a small portion of the new claim fee is sent to them.

  • Actual behavior (code): previousKingPayout is initialized to 0 and never set/credited. All of msg.value is split into platform fee + pot and the previous king gets nothing.

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
uint256 sentAmount = msg.value;
uint256 previousKingPayout = 0; // @> initialized but never assigned a non-zero value
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
// Defensive cap uses previousKingPayout, but it is always 0
if (currentPlatformFee > (sentAmount - previousKingPayout)) {
currentPlatformFee = sentAmount - previousKingPayout; // @> dead branch given previousKingPayout==0 and platformFeePercentage<=100
}
platformFeesBalance = platformFeesBalance + currentPlatformFee;
amountToPot = sentAmount - currentPlatformFee; // @> remainder goes entirely to pot
pot = pot + amountToPot; // @> previous king is never credited anywhere
}

Risk

Likelihood:

  • Always (post-fix): On every claim after the first, the previous king is supposed to be paid but never is, because the payout path is unimplemented.

Impact:

  • Economic/spec deviation: Players are under-rewarded relative to the advertised rules, incentives differ from the spec. The protocol captures more than intended (platform + pot), which can change gameplay dynamics.

  • There is tangible economic divergence from spec but no direct theft relative to the implemented code.

Proof of Concept

What this demonstrates: After two valid claims, the first claimant (previous king) receives no payout: pendingWinnings[prev] == 0.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Game} from "../src/GamePatched.sol"; // only == -> != changed vs original
contract PrevKingPayoutMissingTest is Test {
Game game;
uint256 constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 constant GRACE_PERIOD = 1 days;
uint256 constant FEE_INC = 10;
uint256 constant PLATFORM_FEE = 5;
address p1 = address(0xA11CE);
address p2 = address(0xB0B);
function setUp() public {
game = new Game(INITIAL_CLAIM_FEE, GRACE_PERIOD, FEE_INC, PLATFORM_FEE);
vm.deal(p1, 10 ether);
vm.deal(p2, 10 ether);
}
function test_PreviousKingGetsNoPayout() public {
// p1 becomes king
vm.prank(p1);
game.claimThrone{value: game.claimFee()}();
assertEq(game.pendingWinnings(p1), 0, "baseline: no pending winnings yet");
// p2 overthrows with >= current fee
uint256 fee2 = game.claimFee();
vm.prank(p2);
game.claimThrone{value: fee2}();
// BUG: previous king was not credited at all
assertEq(game.pendingWinnings(p1), 0, "previous king should have been credited per spec");
}
}

Recommended Mitigation

Option A:

Explanation: Adds a configurable previousKingPayoutPercentage, credits the previous king via pull pattern, and enforces prevPct + platformFeePct ≤ 100 to prevent negative pot.

Notes:

  • To match how platformFee is handled, we asume fee calculation is from msg.value, which can exceed claimFee.

  • Consider emitting previousKingPayout (extend ThroneClaimed).

@@
+ uint256 public previousKingPayoutPercentage; // 0-100
@@
- constructor(
- uint256 _initialClaimFee,
- uint256 _gracePeriod,
- uint256 _feeIncreasePercentage,
- uint256 _platformFeePercentage
- ) Ownable(msg.sender) {
+ constructor(
+ uint256 _initialClaimFee,
+ uint256 _gracePeriod,
+ uint256 _feeIncreasePercentage,
+ uint256 _platformFeePercentage,
+ uint256 _prevKingPct
+ ) Ownable(msg.sender) {
...
platformFeePercentage = _platformFeePercentage;
+ require(_prevKingPct <= 100, "Game: prev-king pct must be 0-100");
+ require(_prevKingPct + platformFeePercentage <= 100, "Game: total fee percentages exceed 100");
+ previousKingPayoutPercentage = _prevKingPct;
@@
function claimThrone() external payable gameNotEnded nonReentrant {
...
- uint256 previousKingPayout = 0;
+ uint256 previousKingPayout = 0;
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
+ address prev = currentKing;
- currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
- if (currentPlatformFee > (sentAmount - previousKingPayout)) {
- currentPlatformFee = sentAmount - previousKingPayout;
- }
+ // credit previous king via pull pattern
+ if (prev != address(0) && previousKingPayoutPercentage > 0) {
+ previousKingPayout = (sentAmount * previousKingPayoutPercentage) / 100;
+ pendingWinnings[prev] += previousKingPayout;
+ }
+ currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
platformFeesBalance = platformFeesBalance + currentPlatformFee;
- amountToPot = sentAmount - currentPlatformFee;
+ amountToPot = sentAmount - currentPlatformFee - previousKingPayout;
pot = pot + amountToPot;
...
}
@@
+ // invariant check when owner adjusts platform fee
+ function updatePlatformFeePercentage(
+ uint256 _newPlatformFeePercentage
+ ) external onlyOwner isValidPercentage(_newPlatformFeePercentage) {
+ require(
+ _newPlatformFeePercentage + previousKingPayoutPercentage <= 100,
+ "Game: total fee percentages exceed 100"
+ );
+ platformFeePercentage = _newPlatformFeePercentage;
+ emit PlatformFeePercentageUpdated(_newPlatformFeePercentage);
+ }

Option B

Explanation: Removes unused payout variable and dead conditional; update README/docs to state no previous king payout.

@@
- uint256 previousKingPayout = 0;
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
- currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
- if (currentPlatformFee > (sentAmount - previousKingPayout)) {
- currentPlatformFee = sentAmount - previousKingPayout;
- }
+ currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
platformFeesBalance = platformFeesBalance + currentPlatformFee;
- amountToPot = sentAmount - currentPlatformFee;
+ amountToPot = sentAmount - currentPlatformFee;
pot = pot + amountToPot;

Option B — Align docs to code & remove dead logic

Explanation: Removes unused payout variable and dead conditional; update README/docs to state no previous king payout.

Updates

Appeal created

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

Give us feedback!