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.
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
);
}
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;
emit GameEnded(currentKing, pot, block.timestamp, gameRound);
}
Risk
Likelihood:
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
Three players played to claim thone. 3rd player being the last one becomes the potential legitimate winner.
After passing of the grace period, some one tries to call the declareWinner
function.
However, a malicious attack sees this transaction in the mempool and supersedes this with another claimThrone
transaction.
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);
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);
newTime = block.timestamp + game.getRemainingTime();
vm.warp(newTime + 1);
game.declareWinner();
winnerPendings = game.pendingWinnings(maliciousActor);
assertGt(winnerPendings, 0);
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
}