Last Man Standing

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

Claim Fee Reset Vulnerability

Root + Impact

Description

  • In the normal game flow, the claimThrone() function is designed to implement a progressive difficulty mechanism where the claim fee increases with each successful throne claim.

  • The resetGame() function contains a critical flaw where it resets the claimFee back to initialClaimFee, completely negating the progressive difficulty mechanism established in claimThrone().

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
// assuminng the issue with currentKing is fixed (another report)
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;
// Calculate platform fee
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
// Defensive check to ensure platformFee doesn't exceed available amount after previousKingPayout
if (currentPlatformFee > (sentAmount - previousKingPayout)) {
currentPlatformFee = sentAmount - previousKingPayout;
}
platformFeesBalance = platformFeesBalance + currentPlatformFee;
// Remaining amount goes to the pot
amountToPot = sentAmount - currentPlatformFee;
pot = pot + amountToPot;
// Update game state
currentKing = msg.sender;
lastClaimTime = block.timestamp;
playerClaimCount[msg.sender] = playerClaimCount[msg.sender] + 1;
totalClaims = totalClaims + 1;
// Increase the claim fee for the next player
@> claimFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
emit ThroneClaimed(msg.sender, sentAmount, claimFee, pot, block.timestamp);
}
function resetGame() external onlyOwner gameEndedOnly {
currentKing = address(0);
lastClaimTime = block.timestamp;
pot = 0;
@> claimFee = initialClaimFee;
gracePeriod = initialGracePeriod;
gameEnded = false;
gameRound = gameRound + 1;
// totalClaims is cumulative across rounds, not reset here, but could be if desired.
emit GameReset(gameRound, block.timestamp);
}

Risk

Likelihood:

  • The vulnerability triggers immediately when resetGame() is called, which is expected to happen after each game round.

  • The reset function is accessible to the owner and will be called as part of normal game operation.

Impact:

  • While no direct fund loss occurs, the economic model is disrupted, potentially affecting platform revenue and player incentives.

  • The core game mechanics (progressive difficulty) are compromised, affecting the intended user experience and economic balance.

Proof of Concept

The test proves that the resetGame() function breaks the intended economic model by allowing players to participate in new rounds with artificially low claim fees, undermining the game's progressive difficulty mechanism.

function testGameResetRallbacksCalculatedClaimFeeIncrease() public {
// current values
uint256 initialClaimFee = game.initialClaimFee();
uint256 claimFee = game.claimFee();
address currentKing = game.currentKing();
uint256 lastClaimTime = game.lastClaimTime();
uint256 gracePeriod = game.gracePeriod();
// claim the throne
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
uint256 middleInitialClaimFee = game.initialClaimFee();
uint256 middleClaimFee = game.claimFee();
address newKing = game.currentKing();
// warp time past grace period
vm.warp(lastClaimTime + gracePeriod + 1 hours);
game.declareWinner();
// reset the game
vm.prank(deployer);
game.resetGame();
uint256 afterResetInitialClaimFee = game.initialClaimFee();
uint256 afterResetClaimFee = game.claimFee();
assertNotEq(currentKing, newKing, "currentKing should not be the same as newKing");
assertGt(middleClaimFee, claimFee, "middleClaimFee should be greater than claimFee");
assertEq(middleInitialClaimFee, initialClaimFee, "initialClaimFee after claim is the same as initial");
assertEq(afterResetInitialClaimFee, initialClaimFee, "initialClaimFee after reset is the same as initial");
assertGt(middleClaimFee, afterResetClaimFee, "claimFee after claim is greater than claimFee after reset");
}

Recommended Mitigation

Increase the initialClaimFee based on claimFee and feeIncreasePercentage instead of claimFee during claim of the Throne.

function claimThrone() external payable gameNotEnded nonReentrant {
...
// Increase the claim fee for the next player
- claimFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
+ initialClaimFee = claimFee + (claimFee * feeIncreasePercentage) / 100;
emit ThroneClaimed(msg.sender, sentAmount, claimFee, pot, block.timestamp);
}
Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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