Root + Impact
Description
-
The expected behavior is that the game rewards the previous king with a payout when a new player claims the throne, before updating the current king. This ensures fairness and proper fund distribution.
-
The actual implementation introduces a variable previousKingPayout but sets it to 0 every time without updating it or transferring any funds to the previous king. This makes the payout logic non-functional and misleading in the code. Additionally, the conditional check using previousKingPayout is dead code, creating a false sense of security.
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
);
}
Risk
Likelihood:
Every time a player calls claimThrone(), the logic for paying the previous king is skipped because previousKingPayout is always zero.
The payout code path is dead, so the bug consistently occurs for all transactions.
Impact:
Previous king never receives a payout, breaking core game mechanics and fairness.
Users may lose trust in the platform, potentially causing financial and reputational damage.
Proof of Concept
The claimThrone() function initializes previousKingPayout to 0 every time and never updates it with a real payout amount.
There is no logic to transfer any funds to the previous king, even though the defensive check suggests such a mechanism exists.
function testClaimThrone_NoPayoutToPreviousKing() public {
vm.deal(playerA, 1 ether);
vm.prank(playerA);
game.claimThrone{value: 1 ether}();
vm.deal(playerB, 2 ether);
vm.prank(playerB);
game.claimThrone{value: 2 ether}();
assertEq(playerA.balance, 0 ether);
}
Recommended Mitigation
- uint256 previousKingPayout = 0; // Remove hardcoded zero
- if (currentPlatformFee > (sentAmount - previousKingPayout)) {
- currentPlatformFee = sentAmount - previousKingPayout;
- }
+ // Calculate payout for previous king before updating the state
+ uint256 previousKingPayout = (sentAmount * previousKingSharePercentage) / 100;
+ address payable oldKing = payable(currentKing);
+ if (previousKingPayout > 0 && oldKing != address(0)) {
+ (bool success, ) = oldKing.call{value: previousKingPayout}("");
+ require(success, "Game: Previous king payout failed");
+ }
+ // Ensure platform fee does not exceed remaining amount after previousKingPayout
+ currentPlatformFee = (sentAmount * platformFeePercentage) / 100;
+ if (currentPlatformFee > (sentAmount - previousKingPayout)) {
+ currentPlatformFee = sentAmount - previousKingPayout;
+ }