Last Man Standing

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

Claim Fee Calculation Based on Minimum Required Instead of Actual Payment

Summary

The claimThrone() function calculates the next claim fee based on the minimum required fee rather than the actual amount sent by the player. This creates an economic vulnerability where players can invest large amounts but the next challenger only needs to pay a small increase based on the original minimum fee, making strategic overpayment worthless and breaking the core "Last Man Standing" game mechanics.

Description

The vulnerability exists in the fee calculation logic within claimThrone()

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
uint256 sentAmount = msg.value; // Could be much higher than claimFee
// ... processing logic uses sentAmount ...
// BUG: Next fee calculated from minimum required, not actual payment
claimFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
// ^^^^^^^^ ^^^^^^^^ - Uses old minimum, ignores sentAmount
}

The Problem:

  1. Player pays significantly more than required (strategic overpayment)

  2. All excess payment goes to pot/fees but provides no strategic protection

  3. Next challenger only pays a small increase based on the original minimum

  4. Large investments become economically irrational and strategically worthless

Expected Behavior (Last Man Standing Games):
The next claim fee should be based on what was actually paid, making overpayment strategically valuable by creating higher barriers for challengers.

Current Broken Behavior:
Overpayments are wasted, providing no protection against dethroning despite players investing significantly more.

Proof Of Concept

function test_claimFee_should_be_increased_by_percentage_from_sentAmount() public {
// setting initial fee to 1ETH in constructor for simplicity
// uint256 public constant INITIAL_CLAIM_FEE = 1 ether;
// Initial state: claimFee = 1 ETH
uint256 initialClaimFee = game.claimFee();
assertEq(initialClaimFee, 1 ether);
// STEP 1: Player1 makes strategic investment - pays 5 ETH instead of 1 ETH
// Player1 expects this will make it much harder for challengers
vm.startPrank(player1);
game.claimThrone{value: 5 ether}(); // 5x overpayment for protection
vm.stopPrank();
assertEq(game.currentKing(), player1);
// STEP 2: Check what the next challenger must pay
uint256 actualNextFee = game.claimFee();
// BROKEN: Fee only increased by percentage of original 1 ETH
uint256 expectedBrokenFee = initialClaimFee + (initialClaimFee * game.feeIncreasePercentage()) / 100;
// With 10% increase: 1 + (1 * 10)/100 = 1.1 ETH
assertEq(actualNextFee, expectedBrokenFee); // Currently 1.1 ETH
console2.log("Player1 invested: 5 ETH");
console2.log("Next challenger only needs: ", actualNextFee); // Only 1.1 ETH!
// STEP 3: Player2 easily dethrones Player1 for minimal cost
vm.startPrank(player2);
game.claimThrone{value: actualNextFee}(); // Just 1.1 ETH vs Player1's 5 ETH
vm.stopPrank();
assertEq(game.currentKing(), player2);
// RESULT: Player1 lost 5 ETH investment, Player2 only paid 1.1 ETH
// Player1's 4.9 ETH overpayment provided zero strategic benefit
}

Impact

  • Economic Irrationality: Large investments provide minimal protection, making strategic overpayment worthless

  • Broken Game Mechanics: "Last Man Standing" escalation doesn't work as intended

  • User Financial Loss: Players waste significant ETH on overpayments that provide no benefit

Mitigation

  • Recommended Fix: Base Fee Calculation on Actual Payment

    • In this implementation `feeIncreasePercentage` will act as min increase amount above current king payed amount

  • Another Fix:Don't allow excess payments

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;
uint256 currentPlatformFee = 0;
uint256 amountToPot = 0;
// ... existing platform fee and pot logic ...
// Update game state
currentKing = msg.sender;
lastClaimTime = block.timestamp;
playerClaimCount[msg.sender] = playerClaimCount[msg.sender] + 1;
totalClaims = totalClaims + 1;
// FIXED: Calculate next fee based on actual payment, not minimum required
+ claimFee = sentAmount + (sentAmount * feeIncreasePercentage) / 100;
emit ThroneClaimed(msg.sender, sentAmount, claimFee, pot, block.timestamp);
}
Updates

Appeal created

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

Overpayment not refunded, included in pot, but not in claim fee

Support

FAQs

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