Last Man Standing

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

Game.sol - MEV Front-Running Vulnerability Prevents Fair Winner Declaration

Description

The current game mechanics allow MEV bots to front-run legitimate declareWinner() calls by claiming the throne just before legit winner is declared.
This creates an unfair MEV extraction opportunity where bots can monitor the mempool for winner declarations and immediately claim the throne with higher gas fees, preventing legitimate players from ending the game and declaring winners.

Root Cause

The issue stems from the lack of protection against claims after the grace period has expired:

function claimThrone() external payable gameNotEnded nonReentrant {
// No check to prevent claims after grace period expires
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.");
// ... function continues
}
function declareWinner() external gameNotEnded {
require(
block.timestamp > lastClaimTime + gracePeriod,
"Game: Grace period has not expired yet."
);
// Can be front-run by claimThrone() which resets lastClaimTime
}

Key issues:

  1. claimThrone() can be called even after grace period expires

  2. MEV bots can monitor mempool for declareWinner() transactions

  3. Bots front-run with higher gas claimThrone() calls

  4. This resets lastClaimTime, making declareWinner() revert

  5. Creates an MEV auction for throne claims instead of fair game resolution

Risk

Likelihood: High - MEV bots actively monitor Ethereum mempool for profitable opportunities, and this attack is easily automated.

Impact: Medium - While no funds are directly at risk, the game becomes unplayable for regular users and winners are determined by gas bidding rather than game mechanics.

Proof of Concept

This test demonstrates the vulnerability by showing that claimThrone() can be called after the grace period expires, preventing legitimate winner declaration.

As MEV is not practicaly to demonstrate on foundry this test is just simulating the attack flow

function test_MEVVulnerabilityExists() public {
// Setup game and player becomes king
vm.startPrank(deployer);
game = new Game(0.1 ether, 1 hours, 10, 3);
vm.stopPrank();
vm.deal(player1, 1 ether);
vm.prank(player1);
game.claimThrone{value: 0.1 ether}();
// Wait for grace period to expire - player1 should be the winner
vm.warp(block.timestamp + 1 hours + 1);
// Verify grace period has expired
assertTrue(
block.timestamp > game.lastClaimTime() + game.gracePeriod(),
"Grace period should be expired"
);
// At this point, player1 SHOULD be declared the winner
// But the vulnerability allows new claims even after grace period expires
address mevBot = address(0x777);
vm.deal(mevBot, 10 ether);
// Critical vulnerability: claimThrone() works even after grace period expires
vm.startPrank(mevBot);
game.claimThrone{value: game.claimFee()}(); // This should NOT be allowed
vm.stopPrank();
// The MEV bot is now king, stealing the victory from player1
assertEq(game.currentKing(), mevBot, "MEV bot became king after grace period");
// Player1 who should have won gets nothing
assertEq(game.pendingWinnings(player1), 0, "Player1 has no winnings despite waiting full grace period");
// This demonstrates the core issue: legitimate winners can be displaced
// In a real blockchain environment, MEV bots would monitor mempool for
// declareWinner() transactions and front-run them with claimThrone()
}

Real-world attack scenario:

  1. Grace period expires, player1 should win

  2. Alice submits declareWinner() transaction to mempool

  3. MEV bot detects this and submits claimThrone() with higher gas fee

  4. Bot's transaction executes first, resetting lastClaimTime

  5. Alice's declareWinner() reverts due to "Grace period has not expired yet"

  6. Bot becomes king, stealing the victory from the legitimate winner

Recommended Mitigation

Prevent throne claims after the grace period has expired:

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(
+ block.timestamp <= lastClaimTime + gracePeriod,
+ "Game: Grace period expired, winner must be declared"
+ );
uint256 sentAmount = msg.value;
// ... rest of function remains unchanged
}

Additional recommendation to prevent last-minute MEV battles and spam:

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(
+ block.timestamp <= lastClaimTime + gracePeriod,
+ "Game: Grace period expired, winner must be declared"
+ );
+
+ // Optional: Prevent the same player from claiming multiple times in succession
+ // This reduces MEV bot spam and creates fairer gameplay
+ require(msg.sender != lastKing, "Game: You were the previous king, wait for another player");
uint256 sentAmount = msg.value;
// ... existing code ...
+ // Track the previous king before updating current king
+ lastKing = currentKing;
currentKing = msg.sender;
// ... rest of function
}

Add the new state variable:

contract Game is Ownable {
address public currentKing;
+ address public lastKing; // Track previous king to prevent immediate reclaims
// ... other state variables
}
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

MEV/Frontrunning/Sniping

Support

FAQs

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