Last Man Standing

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

Surplus Eth sent to contract can't be withdrawn or removed by anyone, resulting in stuck ETH forever

Root + Impact

Description

  • There are ways surplus ETH could be sent, either by calling Game::claimThrone , which includes a require statement that msg.value could be more than claimFee permitting surplus ETH to be sent or directly sending ETH to the contract.

  • The protocol doesn't provide a way to withdraw stuck ETH sent to the contract , the surplus accounted ETH do not have a way to be removed, Hence it will be stuck forever.

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;
// 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
);
}

Risk

Likelihood:

  • This happens when a user sends surplus ETH more than the claimFee required in Game::claimThrone , the Protocol has provides no way of sending the surplus back

  • This would also occur when a user sends ETH directly to the contract courtesy of receive function which enables it to receive the surplus ETH

Impact:

  • Users funds would be lost forever as the protocol or the user won't be able to withdraw surplus ETH

Proof of Concept

First Instance - User A sends 4 ether directly into the game contract, which ends up being lost forever

Second Instance invovles users sending more than current claim fee in a game round , assuming claim fee is set as 0.1 ether , someone could send more value than required

Test:
function testPeopleCanSendMoreThanClaimFee() public {
vm.startPrank(player1);
game.claimThrone{value: 2e18}();
}
Result:
Ran 1 test for test/Game.t.sol:GameTest
[PASS] testPeopleCanSendMoreThanClaimFee() (gas: 149826)
Traces:
[169726] GameTest::testPeopleCanSendMoreThanClaimFee()
├─ [0] VM::startPrank(player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84])
│ └─ ← [Return]
├─ [152621] Game::claimThrone{value: 2000000000000000000}()
│ ├─ emit ThroneClaimed(newKing: player1: [0x7026B763CBE7d4E72049EA67E89326432a50ef84], claimAmount: 2000000000000000000 [2e18], newClaimFee: 110000000000000000 [1.1e17], newPot: 1900000000000000000 [1.9e18], timestamp: 1)
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 23.72ms (2.08ms CPU time)

Recommended Mitigation

To tackle both instances, the protocol can send excess eth back to users and collect only the required amount

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 excessAmount = msg.value - claimFee;
+ uint256 sentAmount = claimFee;
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;
+ if (excessAmount > 0) {
+ (bool success, ) = msg.sender.call{value: excessAmount}("");
+ require(success, "ETH refund failed");
+ }
emit ThroneClaimed(msg.sender, sentAmount, claimFee, pot, block.timestamp);
}
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.