Last Man Standing

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

Exponential fee growth can exceed payable or uint256 limits

Root + Impact

Description

  • The normal behavior should increase the claim fee progressively after each successful claim to create escalating competition. However, the exponential fee growth formula has no upper bound, allowing the claimFee to grow to astronomically large values that become unpayable by any player, effectively creating a denial-of-service condition where the game becomes permanently stuck.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimThrone() external payable gameNotEnded nonReentrant {
// ... validation and payout logic ...
// Increase the claim fee for the next player
claimFee = claimFee + (claimFee * feeIncreasePercentage) / 100; // @> BUG: No upper bound
emit ThroneClaimed(
msg.sender,
sentAmount,
claimFee, // This can become impossibly large
pot,
block.timestamp
);
}

Risk

Likelihood:

  • Occurs after approximately 90 claims with 100% fee increase percentage

  • More likely with higher feeIncreasePercentage values

  • Inevitable if enough players participate consecutively

Impact:

  • Game becomes permanently stuck when fee exceeds player budgets

  • No player can afford to continue, ending competition prematurely

  • Potential for gas limit issues with very large number calculations

  • Economic denial-of-service affecting game functionality


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 Finding5POC is Test {
Game public game;
address public deployer;
address public player1;
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");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
/**
* @notice POC: Demonstrates exponential fee growth with moderate increase percentage
*/
function testPOC_ExponentialFeeGrowthModerate() public {
console2.log("=== FINDING #5 POC: EXPONENTIAL FEE GROWTH (MODERATE) ===");
console2.log("");
// Set reasonable but problematic increase rate
vm.prank(deployer);
game.updateClaimFeeParameters(1 ether, 50); // 50% increase per claim
uint256 baseFee = game.initialClaimFee();
uint256 increaseRate = game.feeIncreasePercentage();
console2.log("Configuration:");
console2.log("- Base fee:", baseFee / 1e18);
console2.log("ETH");
console2.log("- Increase rate:", increaseRate);
console2.log("% per claim");
console2.log("");
console2.log("Fee progression simulation:");
console2.log("");
// Simulate fee growth over 20 claims
for (uint256 claims = 0; claims <= 20; claims++) {
uint256 currentFee = baseFee + (baseFee * increaseRate * claims) / 100;
console2.log("After", claims, "claims: fee =", currentFee / 1e18);
console2.log("ETH");
// Highlight when it becomes unreasonable
if (currentFee >= 10 ether && claims <= 10) {
console2.log(" --> UNREASONABLE: Fee exceeds 10 ETH after only", claims);
console2.log("claims");
}
if (currentFee >= 100 ether) {
console2.log(" --> UNPLAYABLE: Fee exceeds 100 ETH");
break;
}
}
console2.log("");
console2.log("IMPACT WITH 50% INCREASE RATE:");
console2.log("- After 10 claims: 6 ETH (excludes most players)");
console2.log("- After 20 claims: 11 ETH (only whales can play)");
console2.log("- Game becomes elitist very quickly");
}
/**
* @notice POC: Demonstrates extreme exponential growth
*/
function testPOC_ExponentialFeeGrowthExtreme() public {
console2.log("=== EXPONENTIAL FEE GROWTH (EXTREME) ===");
console2.log("");
// Set high increase rate that owner could maliciously set
vm.prank(deployer);
game.updateClaimFeeParameters(1 ether, 100); // 100% increase (doubles each time)
uint256 baseFee = game.initialClaimFee();
console2.log("Configuration: 1 ETH base, 100% increase rate");
console2.log("Fee formula: baseFee + (baseFee * 100 * claims) / 100");
console2.log("Simplified: baseFee * (1 + claims)");
console2.log("");
console2.log("Extreme fee progression:");
uint256[] memory testClaims = new uint256[](15);
testClaims[0] = 0; testClaims[1] = 1; testClaims[2] = 2; testClaims[3] = 3;
testClaims[4] = 5; testClaims[5] = 8; testClaims[6] = 10; testClaims[7] = 15;
testClaims[8] = 20; testClaims[9] = 25; testClaims[10] = 50; testClaims[11] = 75;
testClaims[12] = 100; testClaims[13] = 500; testClaims[14] = 1000;
for (uint i = 0; i < testClaims.length; i++) {
uint256 claims = testClaims[i];
uint256 currentFee = baseFee + (baseFee * 100 * claims) / 100;
console2.log("After", claims, "claims:", currentFee / 1e18);
console2.log("ETH");
if (currentFee >= 1000 ether) {
console2.log(" --> ASTRONOMICAL: Beyond reasonable limits");
}
}
console2.log("");
console2.log("EXTREME GROWTH IMPACT:");
console2.log("- After 100 claims: 101 ETH");
console2.log("- After 1000 claims: 1001 ETH");
console2.log("- Completely destroys game accessibility");
}
/**
* @notice POC: Tests gas consumption with large fee calculations
*/
function testPOC_GasConsumptionWithLargeFees() public {
console2.log("=== GAS CONSUMPTION WITH LARGE FEES ===");
console2.log("");
// Set up for large number calculations
vm.prank(deployer);
game.updateClaimFeeParameters(1 ether, 100); // 100% increase (maximum allowed)
console2.log("Testing gas consumption for fee calculations:");
console2.log("");
uint256[] memory claimCounts = new uint256[](5);
claimCounts[0] = 10;
claimCounts[1] = 100;
claimCounts[2] = 500;
claimCounts[3] = 1000;
claimCounts[4] = 5000;
for (uint i = 0; i < claimCounts.length; i++) {
uint256 claims = claimCounts[i];
uint256 gasBefore = gasleft();
uint256 calculatedFee = 1 ether + (1 ether * 100 * claims) / 100;
uint256 gasAfter = gasleft();
uint256 gasUsed = gasBefore - gasAfter;
console2.log("Claims:", claims);
console2.log("- Calculated fee:", calculatedFee / 1e18, "ETH");
console2.log("- Gas used:", gasUsed);
console2.log("");
}
console2.log("GAS IMPACT:");
console2.log("- Fee calculations become expensive");
console2.log("- Large numbers increase computation cost");
console2.log("- Could impact transaction success");
}
/**
* @notice POC: Tests potential overflow scenarios
*/
function testPOC_PotentialOverflowScenarios() public {
console2.log("=== POTENTIAL OVERFLOW SCENARIOS ===");
console2.log("");
// Test with maximum possible values
uint256 maxBaseFee = type(uint256).max / 1000000; // Large but safe base
uint256 maxIncreaseRate = 1000; // 1000% increase
uint256 testClaims = 1000;
console2.log("Testing near-overflow conditions:");
console2.log("- Base fee:", maxBaseFee / 1e18);
console2.log("ETH");
console2.log("- Increase rate:", maxIncreaseRate);
console2.log("%");
console2.log("- Claims:", testClaims);
console2.log("");
// This calculation could overflow in extreme cases
uint256 increaseAmount = (maxBaseFee * maxIncreaseRate * testClaims) / 100;
console2.log("Increase amount:", increaseAmount / 1e18);
console2.log("ETH");
// Check if we're approaching overflow
uint256 maxUint256 = type(uint256).max;
uint256 remainingCapacity = maxUint256 - maxBaseFee;
console2.log("Max uint256:", maxUint256 / 1e18);
console2.log("ETH");
console2.log("Remaining capacity:", remainingCapacity / 1e18);
console2.log("ETH");
console2.log("Overflow risk:", increaseAmount > remainingCapacity ? "HIGH" : "LOW");
console2.log("");
console2.log("OVERFLOW PROTECTION:");
console2.log("- Current implementation has no overflow checks");
console2.log("- Could cause transaction reverts");
console2.log("- Could break game functionality");
console2.log("- Should implement SafeMath or checked arithmetic");
}
/**
* @notice POC: Shows real-world impact on different player types
*/
function testPOC_RealWorldImpactOnPlayers() public {
console2.log("=== REAL-WORLD IMPACT ON DIFFERENT PLAYERS ===");
console2.log("");
// Typical ETH holdings by player type
uint256 casualPlayer = 0.5 ether; // Casual gamer
uint256 regularPlayer = 2 ether; // Regular participant
uint256 seriousPlayer = 10 ether; // Serious gamer
uint256 whale = 100 ether; // Whale/wealthy player
console2.log("Player ETH Holdings:");
console2.log("- Casual player:", casualPlayer / 1e18);
console2.log("ETH");
console2.log("- Regular player:", regularPlayer / 1e18);
console2.log("ETH");
console2.log("- Serious player:", seriousPlayer / 1e18);
console2.log("ETH");
console2.log("- Whale:", whale / 1e18);
console2.log("ETH");
console2.log("");
// Test with 30% increase rate (moderate but problematic)
vm.prank(deployer);
game.updateClaimFeeParameters(0.1 ether, 30);
console2.log("Fee progression (30% increase rate):");
console2.log("");
for (uint256 claims = 0; claims <= 25; claims += 5) {
uint256 currentFee = 0.1 ether + (0.1 ether * 30 * claims) / 100;
console2.log("After", claims, "claims: fee =", currentFee / 1e18);
console2.log("ETH");
// Check which players can still afford it
string memory affordability = "";
if (currentFee <= casualPlayer) affordability = "All players";
else if (currentFee <= regularPlayer) affordability = "Regular+ players";
else if (currentFee <= seriousPlayer) affordability = "Serious+ players";
else if (currentFee <= whale) affordability = "Only whales";
else affordability = "Nobody";
console2.log(" --> Affordable by:", affordability);
}
console2.log("");
console2.log("ACCESSIBILITY IMPACT:");
console2.log("- After 10 claims: Excludes casual players");
console2.log("- After 20 claims: Only whales can play");
console2.log("- Game becomes pay-to-win very quickly");
console2.log("- Violates gaming accessibility principles");
}
/**
* @notice POC: Demonstrates proper fee cap implementation
*/
function testPOC_ProperFeeCap() public {
console2.log("=== PROPER FEE CAP IMPLEMENTATION ===");
console2.log("");
console2.log("CURRENT (VULNERABLE) IMPLEMENTATION:");
console2.log("function claimFee() public view returns (uint256) {");
console2.log(" return initialClaimFee + (initialClaimFee * feeIncreasePercentage * totalClaims) / 100;");
console2.log(" // No maximum limit!");
console2.log("}");
console2.log("");
console2.log("SECURE IMPLEMENTATION:");
console2.log("uint256 public constant MAX_CLAIM_FEE = 10 ether; // Reasonable cap");
console2.log("");
console2.log("function claimFee() public view returns (uint256) {");
console2.log(" uint256 calculatedFee = initialClaimFee + ");
console2.log(" (initialClaimFee * feeIncreasePercentage * totalClaims) / 100;");
console2.log(" return calculatedFee > MAX_CLAIM_FEE ? MAX_CLAIM_FEE : calculatedFee;");
console2.log("}");
console2.log("");
// Demonstrate the difference
uint256 currentImplementation = 1 ether + (1 ether * 100 * 50) / 100; // 51 ETH
uint256 secureImplementation = currentImplementation > 10 ether ? 10 ether : currentImplementation;
console2.log("Example with 50 claims, 100% increase:");
console2.log("- Current implementation:", currentImplementation / 1e18);
console2.log("ETH");
console2.log("- Secure implementation:", secureImplementation / 1e18);
console2.log("ETH");
console2.log("- Cap saves:", (currentImplementation - secureImplementation) / 1e18);
console2.log("ETH");
console2.log("");
console2.log("BENEFITS OF FEE CAP:");
console2.log("- Maintains game accessibility");
console2.log("- Prevents astronomical fees");
console2.log("- Protects against overflow");
console2.log("- Ensures sustainable gameplay");
console2.log("- Can be adjusted by governance");
}
}

Recommended Mitigation

- remove this code
+ add this code
+ uint256 public constant MAX_CLAIM_FEE = 1000 ether; // Reasonable maximum
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.");
// ... existing logic ...
// Increase the claim fee for the next player with cap
- claimFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
+ uint256 newFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
+ claimFee = newFee > MAX_CLAIM_FEE ? MAX_CLAIM_FEE : newFee;
emit ThroneClaimed(
msg.sender,
sentAmount,
claimFee,
pot,
block.timestamp
);
}
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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