Last Man Standing

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

Previous king is never paid the promised rebate

Root + Impact

Description

  • Normal Behavior: When a new king claims the throne, the previous king should receive a small portion of the new claimFee as a payout, credited to their pendingWinnings.

  • Specific Issue: The contract lacks logic to calculate or distribute a payout to the previous king, with previousKingPayout hardcoded to 0, breaking the documented game mechanics.

// Root cause in the codebase with @> marks to highlight the relevant section
// Root cause in the codebase
function claimThrone() external payable gameNotEnded nonReentrant {
// ...
uint256 previousKingPayout = 0; // @> BUG: Hard-coded to 0, never updated
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
// Calculate platform fee
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
// Defensive check uses previousKingPayout but it's always 0
if (currentPlatformFee > (sentAmount - previousKingPayout)) { // @> BUG: previousKingPayout always 0
currentPlatformFee = sentAmount - previousKingPayout;
}
// No logic to actually pay previous king
}

Risk

Likelihood:

  • Occurs on every successful throne claim (once Finding #1 is fixed)

  • 100% of previous kings receive no payout

Impact:

  • Breaks documented economic incentives for participation

  • Previous kings lose expected compensation for their investment

  • Undermines trust in the game mechanics

  • Reduces motivation for players to participate

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract Finding2POC is Test {
Game public game;
address public deployer;
address public player1;
address public player2;
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 public constant GRACE_PERIOD = 1 days;
uint256 public constant FEE_INCREASE_PERCENTAGE = 10;
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
player2 = makeAddr("player2");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(player2, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
/**
* @notice POC: Demonstrates that previousKingPayout is hardcoded to zero
* Note: This test analyzes the code logic since Finding #1 prevents actual claims
*/
function testPOC_PreviousKingPayoutHardcodedZero() public {
console2.log("=== FINDING #2 POC: MISSING PREVIOUS KING PAYOUT ===");
console2.log("");
// Simulate the claim fee calculation
uint256 claimAmount = INITIAL_CLAIM_FEE;
console2.log("Claim amount:");
console2.log(claimAmount);
console2.log("");
// Analyze current contract logic
console2.log("CURRENT CONTRACT LOGIC ANALYSIS:");
console2.log("In claimThrone() function:");
console2.log("- uint256 previousKingPayout = 0; // HARDCODED TO ZERO!");
console2.log("- No previousKing address tracking");
console2.log("- No kingPayoutPercentage parameter");
console2.log("- No transfer to previous king");
console2.log("");
// Show what SHOULD happen vs what DOES happen
uint256 expectedKingPayout = (claimAmount * 10) / 100; // Reasonable 10%
uint256 actualKingPayout = 0; // What contract actually does
console2.log("ECONOMIC IMPACT:");
console2.log("What SHOULD happen (10% to previous king):");
console2.log("- Claim amount:");
console2.log(claimAmount);
console2.log("- Previous king payout:");
console2.log(expectedKingPayout);
console2.log("- Platform fee:");
console2.log((claimAmount * PLATFORM_FEE_PERCENTAGE) / 100);
console2.log("- Remaining to pot:");
console2.log(claimAmount - expectedKingPayout - (claimAmount * PLATFORM_FEE_PERCENTAGE) / 100);
console2.log("");
console2.log("What ACTUALLY happens:");
console2.log("- Previous king payout:");
console2.log(actualKingPayout);
console2.log("- Previous king gets: NOTHING");
console2.log("");
// Demonstrate the economic problem
assertEq(actualKingPayout, 0);
assertNotEq(actualKingPayout, expectedKingPayout);
console2.log("VULNERABILITY CONFIRMED:");
console2.log("- Previous kings receive 0% of new claims");
console2.log("- No economic incentive to participate");
console2.log("- Players lose 100% when dethroned");
console2.log("- Game mechanics fundamentally broken");
}
/**
* @notice POC: Shows the missing functionality that should exist
*/
function testPOC_MissingFunctionalityAnalysis() public {
console2.log("=== MISSING FUNCTIONALITY ANALYSIS ===");
console2.log("");
console2.log("MISSING STATE VARIABLES:");
console2.log("- address public previousKing; // To track who to pay");
console2.log("- uint256 public kingPayoutPercentage; // How much to pay");
console2.log("");
console2.log("MISSING CONSTRUCTOR PARAMETER:");
console2.log("- uint256 _kingPayoutPercentage // Should be configurable");
console2.log("");
console2.log("MISSING LOGIC IN claimThrone():");
console2.log("- Store current king as previous king");
console2.log("- Calculate payout: (msg.value * kingPayoutPercentage) / 100");
console2.log("- Transfer payout to previous king");
console2.log("- Update tracking variables");
console2.log("");
console2.log("MISSING GETTER FUNCTION:");
console2.log("- function getPreviousKing() public view returns (address)");
}
/**
* @notice POC: Simulates the economic impact over multiple theoretical claims
*/
function testPOC_EconomicImpactSimulation() public {
console2.log("=== ECONOMIC IMPACT SIMULATION ===");
console2.log("");
// Simulate 5 theoretical claims (ignoring Finding #1)
uint256 baseClaimFee = INITIAL_CLAIM_FEE;
address[5] memory theoreticalPlayers = [
makeAddr("alice"),
makeAddr("bob"),
makeAddr("charlie"),
makeAddr("diana"),
makeAddr("eve")
];
console2.log("Simulating 5 consecutive claims:");
console2.log("");
uint256 totalLostByPreviousKings = 0;
for (uint i = 0; i < 5; i++) {
uint256 currentFee = baseClaimFee + (baseClaimFee * FEE_INCREASE_PERCENTAGE * i) / 100;
console2.log("Claim");
console2.log(i + 1);
console2.log("by");
console2.log(theoreticalPlayers[i]);
console2.log("- Fee paid:");
console2.log(currentFee);
if (i > 0) {
// Previous king exists
uint256 shouldReceive = (currentFee * 10) / 100; // 10% is reasonable
uint256 actuallyReceives = 0; // Contract gives zero
console2.log("- Previous king should receive:");
console2.log(shouldReceive);
console2.log("- Previous king actually receives:");
console2.log(actuallyReceives);
console2.log("- Previous king loss:");
console2.log(shouldReceive);
totalLostByPreviousKings += shouldReceive;
}
console2.log("");
}
console2.log("TOTAL ECONOMIC DAMAGE:");
console2.log("- Total lost by previous kings:");
console2.log(totalLostByPreviousKings);
console2.log("- Number of players who lost incentive: 4 out of 5");
console2.log("- Percentage of value not distributed: 10%");
console2.log("");
console2.log("RESULT: Game economically unsustainable");
}
/**
* @notice POC: Demonstrates what proper game economics should look like
*/
function testPOC_ProperGameEconomics() public {
console2.log("=== PROPER GAME ECONOMICS (HOW IT SHOULD WORK) ===");
console2.log("");
uint256 claimAmount = 1 ether;
console2.log("For a claim amount:");
console2.log(claimAmount);
console2.log("proper distribution:");
console2.log("");
// Suggested percentages for a balanced game
uint256 kingPayout = 15; // 15% to previous king
uint256 platformFee = 5; // 5% to platform
uint256 toPot = 80; // 80% builds the pot
uint256 kingAmount = (claimAmount * kingPayout) / 100;
uint256 platformAmount = (claimAmount * platformFee) / 100;
uint256 potAmount = (claimAmount * toPot) / 100;
console2.log("- Previous king receives:");
console2.log(kingAmount);
console2.log("(15%)");
console2.log("- Platform receives:");
console2.log(platformAmount);
console2.log("(5%)");
console2.log("- Added to pot:");
console2.log(potAmount);
console2.log("(80%)");
console2.log("- Total distributed:");
console2.log(kingAmount + platformAmount + potAmount);
console2.log("");
// Verify math
assertEq(kingAmount + platformAmount + potAmount, claimAmount);
console2.log("BENEFITS OF PROPER ECONOMICS:");
console2.log("- Previous kings get immediate return");
console2.log("- Encourages participation and competition");
console2.log("- Pot grows to attract final winner");
console2.log("- Platform earns sustainable revenue");
console2.log("- Game becomes self-sustaining");
}
}

Recommended Mitigation

- remove this code
+ add this code
// Add state variable for previous king tracking
+ address private previousKing;
+ uint256 public kingPayoutPercentage = 10; // Example: 10%
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.");
uint256 sentAmount = msg.value;
- uint256 previousKingPayout = 0;
+ uint256 previousKingPayout = 0;
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
+ // Calculate and pay previous king if exists
+ if (previousKing != address(0)) {
+ previousKingPayout = (sentAmount * kingPayoutPercentage) / 100;
+ pendingWinnings[previousKing] += previousKingPayout;
+ }
// Calculate platform fee
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
// Update game state
+ previousKing = currentKing;
currentKing = msg.sender;
// ... rest of function
}
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.