Last Man Standing

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

Game.sol - Overpayments Are Not Refunded Leading to Permanent Loss

Description

The claimThrone() function accepts any amount greater than or equal to claimFee but provides no refund mechanism for overpayments. When users accidentally send more ETH than required, the entire excess amount is absorbed by the contract, with the platform owner receiving an inflated fee percentage and the remaining going to the pot. This creates an unfair situation where user mistakes permanently benefit the platform and future winners.

Root Cause

The issue stems from using the entire msg.value instead of just the required claimFee:

function claimThrone() external payable gameNotEnded nonReentrant {
require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
// ...
uint256 sentAmount = msg.value; // Uses ENTIRE amount sent
currentPlatformFee = (sentAmount * platformFeePercentage) / 100; // Fee on entire amount
amountToPot = sentAmount - currentPlatformFee; // Excess goes to pot
}

Key issues:

  1. No check for msg.value == claimFee

  2. No refund of excess amount (msg.value - claimFee)

  3. Platform fee calculated on entire overpayment

  4. User gains no advantage from overpaying

Risk

Likelihood: Medium - User errors happen regularly, especially with manual transactions or unfamiliar UIs.

Impact: Low - Only affects users who make mistakes. No protocol risk or exploitation vector.

Impact

Low severity because:

  • Self-inflicted loss requiring user error

  • No malicious exploitation possible

  • Doesn't affect game mechanics or other players

  • Platform owner unfairly benefits from user mistakes

Proof of Concept

This test demonstrates how overpayments result in permanent loss with no benefit to the user:

function test_OverpaymentPermanentLoss() public {
// Setup game with 0.1 ETH initial fee and 3% platform fee
vm.startPrank(deployer);
game = new Game(0.1 ether, 1 hours, 10, 3);
vm.stopPrank();
// User intends to pay 0.1 ETH but accidentally sends 1 ETH (10x)
address user = address(0x123);
vm.deal(user, 10 ether);
uint256 userBalanceBefore = user.balance;
vm.prank(user);
game.claimThrone{value: 1 ether}(); // Overpays by 0.9 ETH!
// User lost entire 1 ETH (no refund of 0.9 ETH excess)
assertEq(userBalanceBefore - user.balance, 1 ether, "Full amount taken");
// Platform owner got 3% of 1 ETH = 0.03 ETH (10x expected 0.003 ETH)
assertEq(game.platformFeesBalance(), 0.03 ether, "Platform got inflated fee");
// Pot got 0.97 ETH instead of 0.097 ETH
assertEq(game.pot(), 0.97 ether, "Pot got excess funds");
// Next player still only needs to pay minimum (overpayment gave no benefit)
assertEq(game.claimFee(), 0.11 ether, "Fee unaffected by overpayment");
// User's 0.9 ETH excess is permanently lost
}

Real-world scenario:

  1. Current claimFee = 0.1 ETH

  2. Sends 1 ETH instead of 0.1 ETH

  3. Loses 0.9 ETH permanently with zero benefit

Recommended Mitigation

Require exact payment to prevent accidental overpayments:

function claimThrone() external payable gameNotEnded nonReentrant {
- require(msg.value >= claimFee, "Game: Insufficient ETH sent to claim the throne.");
+ require(msg.value == claimFee, "Game: Must send exact claim fee amount.");
require(msg.sender != currentKing, "Game: You are already the king. No need to re-claim.");
uint256 sentAmount = msg.value;
// ... rest of function
}

This simple change prevents accidental overpayments, ensures fair platform fee collection

Updates

Appeal created

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

Overpayment not refunded, included in pot, but not in claim fee

Support

FAQs

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