Root + Impact
Description
-
Describe the normal behavior in one or more sentences
Answer: The claimThrone() function should allow any player except the current king to claim the throne by sending at least the required claimFee. A portion of the fee goes to the platform, optionally to the previous king, and the rest to the pot, with the caller becoming the new currentKing and the claimFee increasing for the next claim.
-
Explain the specific issue or problem in one or more sentences
Answer: The require(msg.sender == currentKing, ...) condition incorrectly restricts claimThrone() to the current king, preventing new players from claiming the throne. This is the opposite of the intended logic. As a result, once a king is set, no other player can participate, breaking the game’s core mechanic.
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:
-
Reason 1 // Describe WHEN this will occur (avoid using "if" statements)
Answer: The vulnerability manifests as soon as the gracePeriod expires and a player submits a declareWinner() transaction. An attacker monitoring the Ethereum mempool can detect this transaction and submit a claimThrone() transaction with a higher gas price, ensuring it is mined first, thus becoming the currentKing before the winner is declared.
-
Reason 2
Answer: Ethereum’s public mempool and competitive gas market enable bots to routinely scan for high-value transactions like declareWinner(), especially in a financial game with a potentially large pot, making it highly probable that an attacker exploits this during periods of network activity.
Impact:
-
Impact 1
Answer: An attacker successfully front-runs the declareWinner() call, becoming the currentKing and winning the entire pot, which deprives the legitimate king of their prize and undermines the game’s fairness.
-
Impact 2
Answer: Players lose trust in the game due to the unfair outcome, leading to reduced participation, reputational damage, and potential financial losses from gas fees spent on failed attempts to claim or declare the winner
Proof of Concept
Vulnerability
The claimThrone() function contains this incorrect check:
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
Instead of preventing the current king from re-claiming the throne, it does the opposite: it only allows the current king to claim the throne again, excluding all new participants.
How to Reproduce
Deploy the contract, and then run the following PoC:
pragma solidity ^0.8.20;
interface IGame {
function claimThrone() external payable;
}
contract PoC {
IGame public game;
constructor(address _gameAddress) {
game = IGame(_gameAddress);
}
function tryToClaimThrone() external payable {
require(msg.value >= 1 ether, "Example: Minimum fee not met");
game.claimThrone{value: msg.value}();
}
}
Expected Behavior
After the first player claims the throne, new players should be able to
dethrone the current king by paying the updated claimFee. However, the
require(msg.sender == currentKing) line prevents anyone else from ever
participating.
Recommended Mitigation
Root Cause
Incorrect logic in the access restriction:
require(msg.sender == currentKing, "Game: You are already the king. No need to re-claim.");
This line must be inverted to prevent the current king from reclaiming:
require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
Fix Recommendation
Replace the incorrect require() line in the claimThrone() function with:
require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");