Logical Flaw in claimThrone Prevents Any Player from Becoming King, Making the Game Unplayable
Description
-
Normal Behavior: A new player (msg.sender) who is not the currentKing should be able to call claimThrone() with the required claimFee to become the new currentKing. The game is designed for players to usurp the throne from one another.
-
The Issue: The claimThrone function contains a require statement that is logically inverted. The line require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim."); requires the caller to already be the king. This makes it impossible for any new player to ever claim the throne. The game cannot progress past its initial state where currentKing is address(0).
function claimThrone() external payable gameNotEnded nonReentrant {
require(
msg.value >= claimFee,
"Game: Insufficient ETH sent to claim the throne."
);
require(
"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: HIGH
Impact: HIGH
-
The core function of the game is fundamentally broken.
-
No one can ever become king, no funds can enter the pot (beyond a failed transaction), and the game cannot be played in any capacity. The contract fails to execute its primary purpose.
Proof of Concept
As any user account (e.g., account_A), attempt to call claimThrone() by sending the initialClaimFee.
Observe: The transaction will always revert with the error message "Game: You are already the king. No need to re-claim."
Reason: The check require(msg.sender == currentKing) fails because account_A (msg.sender) is not address(0) (currentKing).
Add this code to the test file:
function testClaimThroneRevert() public {
vm.prank(player1);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
}
Recommended Mitigation
The logical check must be inverted to ensure the caller is not the current king.
Change this line:
function claimThrone() external payable gameNotEnded nonReentrant {
require(
msg.value >= claimFee,
"Game: Insufficient ETH sent to claim the throne."
);
require(
- msg.sender == currentKing,
+ 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
);
}