Last Man Standing

First Flight #45
Beginner FriendlyFoundrySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

claimThrone function can front-run by attacker to claimThrone at lower claim fees and chances of winning higher prize amount

claimThrone function can front-run by attacker to claimThrone at lower claim fees and chances of winning higher prize amount

Description

  • Attackers can exploit the public mempool to front-run updateClaimFeeParameters and updatePlatformFeePercentage transaction by submitting claimThrone transaction with higher gas price, claimming throne at lower claim fees and chance of winning higher prize amount

  • Blockchain transactions are visible in the public mempool before confirmation due to this, if owner tries to update the claimFee parameters (initialClaimFee and feeIncreasePercentage) to higher values. Attacker can submit transaction to claimThrone with lower claimFees

  • The contract lacks mechanisms like time-locks or commit-reveal to delay these actions.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood: high

  • High likelihood due to easy mempool monitoring, automated MEV bots, and strong financial incentives in volatile markets.

Impact: high

  • User trust and platform reputation suffer, risking reduced adoption.

  • The attacker puts more ETH into the pot under the lower fee structure, receiving a larger pot than they would have if they had claimed after the fee increase

Proof of Concept

  • Append the following test to Game.t.sol and run forge test --mt test_claimThroneFrontRun -vvvv

// If owner tries to increase the fee percentage by 5% to 20%
function test_claimThrone_frontRunning() public {
uint256 initialClaimFee = game.claimFee();
assert(initialClaimFee == INITIAL_CLAIM_FEE);
assert(game.feeIncreasePercentage() == 5); // 5%
assert(game.currentKing() == address(0));
assert(game.platformFeesBalance() == 0);
assert(game.pot() == 0);
vm.startPrank(attacker);
game.claimThrone{value:1.1 ether}();
vm.stopPrank();
assert(game.currentKing() == attacker);
assert(game.claimFee() == 1.1 ether);
assert(game.platformFeesBalance() == 0.055 ether);
assert(game.pot() == 1.045 ether);
// owner updates feeIncreasePercentage to 20% increasing the claimFee values
vm.prank(deployer);
game.updateClaimFeeParameters(1.5 ether, 20);
assert(game.initialClaimFee() == 1.5 ether);
assert(game.feeIncreasePercentage() == 20);
vm.prank(user2);
game.claimThrone{value: 1.22 ether}();
assert(game.currentKing() == user2);
assert(game.claimFee() == 1.32 ether);
assert(game.platformFeesBalance() == 0.116 ether);
assert(game.pot() == 2.204 ether);
}

Recommended Mitigation

  • Use time-lock mechanism to prevent the attack.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
contract Game is Ownable {
// New State variables for Time-lock
struct PendingUpdate {
uint256 newInitialClaimFee;
uint256 newFeeIncreasePercentage;
uint256 effectiveTime;
bool isPending;
}
PendingUpdate public pendingClaimFeeParams;
uint256 public constant UPDATE_DELAY = 1 days; // 24 hours delay
// New events for Time-lock
event ClaimFeeParametersUpdateScheduled(
uint256 _newInitialClaimFee, uint256 _newFeeIncreasePercentage, uint256 _effectiveTime
);
event ClaimFeeParametersUpdated(
uint256 newInitialClaimFee, uint256 newFeeIncreasePercentage
);
// Modified updateClaimFeeParameters for Time-Lock
function scheduleClaimFeeParametersUpdate(
uint256 _newInitialClaimFee,
uint256 _newFeeIncreasePercentage
) external onlyOwner isValidPercentage(_newFeeIncreasePercentage) {
require(_newInitialClaimFee > 0, "Game: New initial claim fee must be greater than zero.");
// Set the pending update with a future effective time
pendingClaimFeeParams = PendingUpdate({
newInitialClaimFee: _newInitialClaimFee,
newFeeIncreasePercentage: _newFeeIncreasePercentage,
effectiveTime: block.timestamp + UPDATE_DELAY,
isPending: true
});
emit ClaimFeeParametersUpdateScheduled(
_newInitialClaimFee,
_newFeeIncreasePercentage,
pendingClaimFeeParams.effectiveTime
);
}
function executeClaimFeeParametersUpdate() external onlyOwner {
require(pendingClaimFeeParams.isPending, "Game: No pending update to execute.");
require(block.timestamp >= pendingClaimFeeParams.effectiveTime, "Game: Update not yet effective.");
// Update the game parameters
initialClaimFee = pendingClaimFeeParams.newInitialClaimFee;
feeIncreasePercentage = pendingClaimFeeParams.newFeeIncreasePercentage;
// Reset the pending update
pendingClaimFeeParams.isPending = false;
emit ClaimFeeParametersUpdated(
initialClaimFee,
feeIncreasePercentage
);
}
Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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