Last Man Standing

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

Direct Ether Transfers Become Irretrievable

Summary

The contract implements a receive() function that accepts Ether transfers but provides no mechanism to withdraw these funds. Any Ether sent directly to the contract (outside of claimThrone() calls) becomes permanently locked, as there are no admin functions or withdrawal mechanisms to recover these funds.

Description

The contract contains a bare receive() function:

receive() external payable {}

This function allows the contract to accept Ether sent via:

  • Direct transfers (address.transfer(), address.send())

  • Low-level calls without data (address.call{value: amount}(""))

  • Sending Ether to the contract address from wallets

However, the contract lacks any withdrawal mechanism for these funds

Imapct

  • Permanent Fund Loss: Any Ether sent directly to contract is irretrievable

Proof of Concept

function test_ether_locked_via_receive_function() public {
// STEP 1: User accidentally sends 1 ETH directly to contract
uint256 accidentalAmount = 1 ether;
vm.startPrank(player1);
(bool success, ) = address(game).call{value: accidentalAmount}("");
require(success, "Transfer failed");
vm.stopPrank();
// Contract receives the ETH but doesn't track it anywhere
assertEq(address(game).balance, accidentalAmount);
assertEq(game.pot(), 0); // Not added to pot
assertEq(game.platformFeesBalance(), 0); // Not added to platform fees
// STEP 2: Normal game flow - player2 plays the game
vm.startPrank(player2);
uint256 claimFee = game.claimFee();
game.claimThrone{value: claimFee}();
vm.stopPrank();
assertEq(game.currentKing(), player2);
// STEP 3: Grace period expires, player2 wins
vm.warp(block.timestamp + game.gracePeriod() + 1 seconds);
game.declareWinner();
// STEP 4: All legitimate withdrawals occur
// Winner withdraws pot
vm.startPrank(player2);
game.withdrawWinnings();
vm.stopPrank();
// Admin withdraws platform fees
vm.startPrank(deployer);
game.withdrawPlatformFees();
vm.stopPrank();
// STEP 5: PROOF OF LOCK - The accidentally sent 1 ETH is still trapped
uint256 finalBalance = address(game).balance;
assertEq(finalBalance, 1 ether, "1 ETH permanently locked in contract");
// There is NO function to withdraw this remaining balance
// The 1 ETH sent via receive() is permanently lost
}

Mitigation

  1. Remove receive() Function

    OR

  2. Add Admin Withdrawal Function

Updates

Appeal created

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Direct ETH transfers - User mistake

There is no reason for a user to directly send ETH or anything to this contract. Basic user mistake, info, invalid according to CH Docs.

nem0thefinder Submitter
about 1 month ago
inallhonesty Lead Judge
about 1 month ago
inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Direct ETH transfers - User mistake

There is no reason for a user to directly send ETH or anything to this contract. Basic user mistake, info, invalid according to CH Docs.

Support

FAQs

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