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.");
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;
currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
if (currentPlatformFee > (sentAmount - previousKingPayout)) {
currentPlatformFee = sentAmount - previousKingPayout;
}
platformFeesBalance = platformFeesBalance + currentPlatformFee;
amountToPot = sentAmount - currentPlatformFee;
pot = pot + amountToPot;
currentKing = msg.sender;
lastClaimTime = block.timestamp;
playerClaimCount[msg.sender] = playerClaimCount[msg.sender] + 1;
totalClaims = totalClaims + 1;
@> 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;
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 {
uint256 initialClaimFee = game.initialClaimFee();
uint256 claimFee = game.claimFee();
address currentKing = game.currentKing();
uint256 lastClaimTime = game.lastClaimTime();
uint256 gracePeriod = game.gracePeriod();
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
uint256 middleInitialClaimFee = game.initialClaimFee();
uint256 middleClaimFee = game.claimFee();
address newKing = game.currentKing();
vm.warp(lastClaimTime + gracePeriod + 1 hours);
game.declareWinner();
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);
}