Last Man Standing

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

Percentage allocations can exceed 100% once king payout is implemented

Root + Impact

Description

The normal behavior should ensure that the sum of all percentage allocations (platform fee + king payout) never exceeds 100% of the claim fee. However, once Finding #2 is fixed by adding a kingPayoutPercentage, there is no validation that platformFeePercentage + kingPayoutPercentage ≤ 100. This can cause arithmetic underflow when calculating the pot amount, leading to transaction reverts and game malfunction.

// Root cause in the codebase (after implementing Finding #2 fix)
function claimThrone() external payable gameNotEnded nonReentrant {
// ...
uint256 sentAmount = msg.value;
uint256 previousKingPayout = 0;
// Calculate previous king payout (after Fix #2)
if (previousKing != address(0)) {
previousKingPayout = (sentAmount * kingPayoutPercentage) / 100; // @> Could be large
}
// Calculate platform fee
uint256 currentPlatformFee = (sentAmount * platformFeePercentage) / 100; // @> Could be large
// BUG: No validation that previousKingPayout + currentPlatformFee ≤ sentAmount
uint256 amountToPot = sentAmount - previousKingPayout - currentPlatformFee; // @> Can underflow
pot = pot + amountToPot;
}
// Parameter setters allow dangerous combinations
function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage)
external
onlyOwner
isValidPercentage(_newPlatformFeePercentage) // @> Only checks ≤ 100, not total
{
platformFeePercentage = _newPlatformFeePercentage; // Could be 60%
// If kingPayoutPercentage is 50%, total = 110% - BUG
}

Risk

Likelihood:

  • Occurs when owner misconfigures percentage parameters

  • Higher risk after implementing fix for Finding #2

  • Can happen during parameter updates if not properly validated

Impact:

  • All future throne claims will revert due to arithmetic underflow

  • Game becomes completely unusable until parameters are fixed

  • Players lose trust due to transaction failures

  • Potential loss of ETH through failed transactions (gas costs)

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 Finding6POC 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;
// Move struct declaration to contract level
struct AllocationExample {
string name;
uint256 platformFee;
uint256 kingPayout;
bool isValid;
}
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 current over-allocation possibility
*/
function testPOC_CurrentOverAllocationPossibility() public {
console2.log("=== FINDING #6 POC: PERCENTAGE ALLOCATION OVER 100% ===");
console2.log("");
// Set platform fee to maximum allowed
vm.prank(deployer);
game.updatePlatformFeePercentage(95); // 95% to platform
uint256 platformFee = game.platformFeePercentage();
console2.log("Current platform fee: %d%%", platformFee);
console2.log("");
// Simulate adding kingPayoutPercentage (from Finding #2 fix)
uint256 simulatedKingPayout = 20; // 20% to previous king (reasonable)
uint256 totalPercentage = platformFee + simulatedKingPayout;
console2.log("SIMULATED SCENARIO (after Finding #2 fix):");
console2.log("- Platform fee: %d%%", platformFee);
console2.log("- King payout: %d%%", simulatedKingPayout);
console2.log("- Total allocation: %d%%", totalPercentage);
console2.log("");
// Demonstrate over-allocation
if (totalPercentage > 100) {
console2.log("OVER-ALLOCATION DETECTED!");
console2.log("- Exceeds 100%% by: %d%%", totalPercentage - 100);
}
// Show the mathematical problem
uint256 testClaim = 1 ether;
uint256 platformAmount = (testClaim * platformFee) / 100;
uint256 kingAmount = (testClaim * simulatedKingPayout) / 100;
uint256 totalAllocated = platformAmount + kingAmount;
uint256 remainingForPot = testClaim > totalAllocated ? testClaim - totalAllocated : 0;
console2.log("MATHEMATICAL BREAKDOWN (1 ETH claim):");
console2.log("- Claim amount: %d ETH", testClaim / 1e18);
console2.log("- Platform gets: %d.%d ETH", platformAmount / 1e18, (platformAmount % 1e18) / 1e17);
console2.log("- King gets: %d.%d ETH", kingAmount / 1e18, (kingAmount % 1e18) / 1e17);
console2.log("- Total allocated: %d.%d ETH", totalAllocated / 1e18, (totalAllocated % 1e18) / 1e17);
console2.log("- Remaining for pot: %d.%d ETH", remainingForPot / 1e18, (remainingForPot % 1e18) / 1e17);
console2.log("");
if (totalAllocated > testClaim) {
console2.log("PROBLEM: Trying to allocate more than exists!");
console2.log("- Over-allocation: %d.%d ETH", (totalAllocated - testClaim) / 1e18, ((totalAllocated - testClaim) % 1e18) / 1e17);
}
// Verify the issue
assertGt(totalPercentage, 100);
assertGt(totalAllocated, testClaim);
console2.log("VULNERABILITY CONFIRMED: Over-allocation possible");
}
/**
* @notice POC: Shows extreme over-allocation scenarios
*/
function testPOC_ExtremeOverAllocation() public {
console2.log("=== EXTREME OVER-ALLOCATION SCENARIOS ===");
console2.log("");
// Scenario 1: Maximum platform fee + reasonable king payout
console2.log("SCENARIO 1: Max platform fee + reasonable king payout");
uint256 platformFee1 = 100; // Maximum
uint256 kingPayout1 = 15; // Reasonable
uint256 total1 = platformFee1 + kingPayout1;
console2.log("- Platform: %d%%, King: %d%%, Total: %d%%", platformFee1, kingPayout1, total1);
console2.log("- Over-allocation: %d%%", total1 - 100);
console2.log("");
// Scenario 2: High platform fee + high king payout
console2.log("SCENARIO 2: High platform fee + high king payout");
uint256 platformFee2 = 80;
uint256 kingPayout2 = 50;
uint256 total2 = platformFee2 + kingPayout2;
console2.log("- Platform: %d%%, King: %d%%, Total: %d%%", platformFee2, kingPayout2, total2);
console2.log("- Over-allocation: %d%%", total2 - 100);
console2.log("");
// Scenario 3: Both fees very high
console2.log("SCENARIO 3: Both fees very high");
uint256 platformFee3 = 75;
uint256 kingPayout3 = 75;
uint256 total3 = platformFee3 + kingPayout3;
console2.log("- Platform: %d%%, King: %d%%, Total: %d%%", platformFee3, kingPayout3, total3);
console2.log("- Over-allocation: %d%%", total3 - 100);
console2.log("");
// Show financial impact
uint256 claimAmount = 2 ether;
console2.log("FINANCIAL IMPACT (2 ETH claim in Scenario 3):");
uint256 platformAmount = (claimAmount * platformFee3) / 100;
uint256 kingAmount = (claimAmount * kingPayout3) / 100;
uint256 totalRequired = platformAmount + kingAmount;
console2.log("- Platform needs: %d.%d ETH", platformAmount / 1e18, (platformAmount % 1e18) / 1e17);
console2.log("- King needs: %d.%d ETH", kingAmount / 1e18, (kingAmount % 1e18) / 1e17);
console2.log("- Total needed: %d.%d ETH", totalRequired / 1e18, (totalRequired % 1e18) / 1e17);
console2.log("- Actually available: %d ETH", claimAmount / 1e18);
console2.log("- Shortfall: %d.%d ETH", (totalRequired - claimAmount) / 1e18, ((totalRequired - claimAmount) % 1e18) / 1e17);
assertGt(totalRequired, claimAmount);
}
/**
* @notice POC: Demonstrates contract insolvency scenarios
*/
function testPOC_ContractInsolvencyScenarios() public {
console2.log("=== CONTRACT INSOLVENCY SCENARIOS ===");
console2.log("");
// Set up over-allocation scenario
vm.prank(deployer);
game.updatePlatformFeePercentage(90); // 90% platform fee
// Simulate multiple claims with over-allocation
uint256[] memory claims = new uint256[](5);
claims[0] = 1 ether;
claims[1] = 2 ether;
claims[2] = 0.5 ether;
claims[3] = 3 ether;
claims[4] = 1.5 ether;
uint256 simulatedKingPayout = 25; // 25% to kings
uint256 platformFee = game.platformFeePercentage(); // 90%
uint256 totalPercentage = platformFee + simulatedKingPayout; // 115%
console2.log("Over-allocation setup:");
console2.log("- Platform fee: %d%%", platformFee);
console2.log("- King payout: %d%%", simulatedKingPayout);
console2.log("- Total: %d%%", totalPercentage);
console2.log("");
uint256 totalDeficit = 0;
console2.log("Simulating multiple claims with over-allocation:");
console2.log("");
for (uint i = 0; i < claims.length; i++) {
uint256 claimAmount = claims[i];
uint256 platformAmount = (claimAmount * platformFee) / 100;
uint256 kingAmount = (claimAmount * simulatedKingPayout) / 100;
uint256 totalRequired = platformAmount + kingAmount;
uint256 deficit = totalRequired - claimAmount;
console2.log("Claim %d (%d ETH):", i + 1, claimAmount / 1e18);
console2.log("- Platform gets: %d.%d ETH", platformAmount / 1e18, (platformAmount % 1e18) / 1e17);
console2.log("- King gets: %d.%d ETH", kingAmount / 1e18, (kingAmount % 1e18) / 1e17);
console2.log("- Total required: %d.%d ETH", totalRequired / 1e18, (totalRequired % 1e18) / 1e17);
console2.log("- Deficit: %d.%d ETH", deficit / 1e18, (deficit % 1e18) / 1e17);
console2.log("");
totalDeficit += deficit;
}
console2.log("CUMULATIVE INSOLVENCY:");
console2.log("- Total deficit across all claims: %d.%d ETH", totalDeficit / 1e18, (totalDeficit % 1e18) / 1e17);
console2.log("- Contract cannot fulfill all obligations");
console2.log("- Some payouts will fail");
console2.log("");
console2.log("IMPACT: Contract becomes mathematically insolvent");
}
/**
* @notice POC: Shows underflow risks in pot calculations
*/
function testPOC_UnderflowRisksInPotCalculations() public {
console2.log("=== UNDERFLOW RISKS IN POT CALCULATIONS ===");
console2.log("");
console2.log("VULNERABLE CODE PATTERN:");
console2.log("uint256 platformAmount = (msg.value * platformFeePercentage) / 100;");
console2.log("uint256 kingAmount = (msg.value * kingPayoutPercentage) / 100;");
console2.log("uint256 potAddition = msg.value - platformAmount - kingAmount;");
console2.log("// ^^ This can underflow if platformAmount + kingAmount > msg.value");
console2.log("");
// Simulate the vulnerable calculation
uint256 claimValue = 1 ether;
uint256 platformPercentage = 80;
uint256 kingPercentage = 40; // Total = 120%
uint256 platformAmount = (claimValue * platformPercentage) / 100;
uint256 kingAmount = (claimValue * kingPercentage) / 100;
uint256 totalDeductions = platformAmount + kingAmount;
console2.log("UNDERFLOW SCENARIO:");
console2.log("- Claim value: %d ETH", claimValue / 1e18);
console2.log("- Platform amount: %d.%d ETH", platformAmount / 1e18, (platformAmount % 1e18) / 1e17);
console2.log("- King amount: %d.%d ETH", kingAmount / 1e18, (kingAmount % 1e18) / 1e17);
console2.log("- Total deductions: %d.%d ETH", totalDeductions / 1e18, (totalDeductions % 1e18) / 1e17);
console2.log("");
if (totalDeductions > claimValue) {
console2.log("UNDERFLOW DETECTED!");
console2.log("- Trying to subtract %d.%d ETH from %d ETH", totalDeductions / 1e18, (totalDeductions % 1e18) / 1e17, claimValue / 1e18);
console2.log("- Would cause underflow in pot calculation");
console2.log("- Transaction would revert");
}
// Demonstrate with Solidity 0.8+ automatic overflow protection
console2.log("With Solidity 0.8+ overflow protection:");
console2.log("- Calculation would revert with panic");
console2.log("- claimThrone() function would fail");
console2.log("- Game becomes unplayable");
}
/**
* @notice POC: Shows proper validation implementation
*/
function testPOC_ProperValidationImplementation() public {
console2.log("=== PROPER VALIDATION IMPLEMENTATION ===");
console2.log("");
console2.log("CURRENT (VULNERABLE) IMPLEMENTATION:");
console2.log("function updatePlatformFeePercentage(uint256 _platformFeePercentage) external onlyOwner {");
console2.log(" require(_platformFeePercentage <= 100, 'Platform fee cannot exceed 100%');");
console2.log(" platformFeePercentage = _platformFeePercentage;");
console2.log(" // No check against other percentage allocations!");
console2.log("}");
console2.log("");
console2.log("SECURE IMPLEMENTATION:");
console2.log("function updatePlatformFeePercentage(uint256 _platformFeePercentage) external onlyOwner {");
console2.log(" require(_platformFeePercentage <= 100, 'Platform fee cannot exceed 100%');");
console2.log(" require(_platformFeePercentage + kingPayoutPercentage <= 100, ");
console2.log(" 'Total allocations cannot exceed 100%');");
console2.log(" platformFeePercentage = _platformFeePercentage;");
console2.log("}");
console2.log("");
console2.log("function updateKingPayoutPercentage(uint256 _kingPayoutPercentage) external onlyOwner {");
console2.log(" require(_kingPayoutPercentage <= 100, 'King payout cannot exceed 100%');");
console2.log(" require(platformFeePercentage + _kingPayoutPercentage <= 100,");
console2.log(" 'Total allocations cannot exceed 100%');");
console2.log(" kingPayoutPercentage = _kingPayoutPercentage;");
console2.log("}");
console2.log("");
console2.log("ADDITIONAL SAFETY FEATURES:");
console2.log("- Getter function to check total allocation percentage");
console2.log("- Events when percentages change");
console2.log("- Maximum reasonable limits (e.g., no single fee > 50%)");
console2.log("- Validation in constructor");
}
/**
* @notice POC: Demonstrates practical allocation examples
*/
function testPOC_PracticalAllocationExamples() public {
console2.log("=== PRACTICAL ALLOCATION EXAMPLES ===");
console2.log("");
// Create array of examples (now using the contract-level struct)
AllocationExample[6] memory examples;
examples[0] = AllocationExample("Conservative", 5, 10, true); // 15% total
examples[1] = AllocationExample("Balanced", 10, 15, true); // 25% total
examples[2] = AllocationExample("Platform Heavy", 25, 10, true); // 35% total
examples[3] = AllocationExample("King Heavy", 10, 30, true); // 40% total
examples[4] = AllocationExample("Aggressive", 40, 40, true); // 80% total
examples[5] = AllocationExample("Over-allocated", 60, 50, false); // 110% total
uint256 testClaim = 1 ether;
console2.log("Testing different allocation strategies (1 ETH claim):");
console2.log("");
for (uint i = 0; i < examples.length; i++) {
AllocationExample memory example = examples[i];
uint256 total = example.platformFee + example.kingPayout;
uint256 platformAmount = (testClaim * example.platformFee) / 100;
uint256 kingAmount = (testClaim * example.kingPayout) / 100;
uint256 potAmount = total <= 100 ? testClaim - platformAmount - kingAmount : 0;
console2.log("%s allocation:", example.name);
console2.log("- Platform: %d%% = %d.%d ETH", example.platformFee, platformAmount / 1e18, (platformAmount % 1e18) / 1e17);
console2.log("- King: %d%% = %d.%d ETH", example.kingPayout, kingAmount / 1e18, (kingAmount % 1e18) / 1e17);
console2.log("- To pot: %d.%d ETH", potAmount / 1e18, (potAmount % 1e18) / 1e17);
string memory validityText = example.isValid ? "(Valid)" : "(INVALID)";
console2.log("- Total %%: %d %s", total, validityText);
console2.log("");
}
console2.log("RECOMMENDATIONS:");
console2.log("- Keep total allocations under 50% for healthy pot growth");
console2.log("- Platform fee: 5-15% (sustainable revenue)");
console2.log("- King payout: 10-25% (good incentive)");
console2.log("- Remaining 60-85% grows the pot for final winner");
}
}

Recommended Mitigation

- remove this code
+ add this code
// Add validation when implementing Finding #2 fix
+ uint256 public kingPayoutPercentage = 10; // Example: 10%
+ modifier validTotalPercentage() {
+ require(
+ platformFeePercentage + kingPayoutPercentage <= 100,
+ "Game: Total percentages exceed 100%"
+ );
+ _;
+ }
function updatePlatformFeePercentage(uint256 _newPlatformFeePercentage)
external
onlyOwner
isValidPercentage(_newPlatformFeePercentage)
+ validTotalPercentage
{
platformFeePercentage = _newPlatformFeePercentage;
emit PlatformFeePercentageUpdated(_newPlatformFeePercentage);
}
+ function updateKingPayoutPercentage(uint256 _newKingPayoutPercentage)
+ external
+ onlyOwner
+ isValidPercentage(_newKingPayoutPercentage)
+ validTotalPercentage
+ {
+ kingPayoutPercentage = _newKingPayoutPercentage;
+ emit KingPayoutPercentageUpdated(_newKingPayoutPercentage);
+ }
function claimThrone() external payable gameNotEnded nonReentrant {
// ... existing validation ...
uint256 sentAmount = msg.value;
uint256 previousKingPayout = 0;
uint256 currentPlatformFee = 0;
// Calculate payouts with validation
if (previousKing != address(0)) {
previousKingPayout = (sentAmount * kingPayoutPercentage) / 100;
}
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
+ // Ensure we don't over-allocate
+ require(
+ previousKingPayout + currentPlatformFee <= sentAmount,
+ "Game: Percentage allocation exceeds claim amount"
+ );
uint256 amountToPot = sentAmount - previousKingPayout - currentPlatformFee;
pot = pot + amountToPot;
// ... rest of function
}
Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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