Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

Total DoS in `claimThrone` function


Description

The claimThrone function allows players to claim the throne by sending ETH greater than or equal to the current claimFee, updating the currentKing, increasing the pot, and incrementing the claimFee for the next claim. A platform fee is deducted from the sent amount, and the game state is updated with the new king and claim details.

The function contains a critical bug in the require statement that checks if the caller is the currentKing. The condition require(msg.sender == currentKing, ...) incorrectly allows only the currentKing to claim the throne again, but since currentKing is initialized as address(0), no player can ever satisfy this condition, preventing any claims and causing a complete Denial of Service (DoS) that renders the game unplayable.

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;
// 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
);
}

Risk

Likelihood:

  • The issue occurs immediately upon deployment when any player attempts to call claimThrone, as currentKing is initialized to address(0).

  • Every subsequent attempt to claim the throne by any player will fail, as no player’s address can match address(0).

Impact:

  • The game is completely unplayable, as no player can claim the throne, preventing the game from starting or progressing.

  • Players lose trust in the contract, and any ETH sent in failed transactions is wasted on gas fees, discouraging participation.

Proof of Concept

The following unit test demonstrates the critical DoS vulnerability in the claimThrone function, where the incorrect require(msg.sender == currentKing, ...) statement prevents any player from claiming the throne, rendering the game unplayable. The test is designed to show that both an initial and subsequent claim attempts fail, leaving the game state unchanged and confirming the total lockout of the game.

Paste this unit test in the test file:

// Incorrect require statement in claimThrone prevents any player from claiming, causing total DoS
function testDenialOfServiceNoPlayerCanClaimThrone() public {
// Player1 attempts to claim the throne
vm.prank(player1);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// Verify state unchanged
assertEq(game.currentKing(), address(0), "Current king should still be address(0)");
assertEq(game.pot(), 0, "Pot should still be 0");
assertEq(game.claimFee(), INITIAL_CLAIM_FEE, "Claim fee should not change");
assertEq(game.playerClaimCount(player1), 0, "Player1 should still have no claims");
assertEq(game.totalClaims(), 0, "Total claims should still be 0");
// Player2 attempts to claim the throne
vm.prank(player2);
vm.expectRevert("Game: You are already the king. No need to re-claim.");
game.claimThrone{value: INITIAL_CLAIM_FEE}();
// Verify state still unchanged
assertEq(game.currentKing(), address(0), "Current king should still be address(0)");
assertEq(game.pot(), 0, "Pot should still be 0");
assertEq(game.claimFee(), INITIAL_CLAIM_FEE, "Claim fee should not change");
assertEq(game.playerClaimCount(player2), 0, "Player2 should have no claims");
assertEq(game.totalClaims(), 0, "Total claims should still be 0");

}

## Recommended Mitigation
Correct the `require` statement to prevent the current king from re-claiming the throne, allowing new players to claim it. This ensures the first player can claim when `currentKing` is `address(0)` and subsequent players can claim when they are not the `currentKing`.
```diff
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.");
+ require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
uint256 sentAmount = msg.value;
// ... rest of the function unchanged
}

Updates

Appeal created

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Game::claimThrone `msg.sender == currentKing` check is busted

Support

FAQs

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

Give us feedback!