Last Man Standing

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

Stuck ETH Vulnerability in the Contract

Root + Impact

Unvalidated receive () function allows ETH to be sent directly to the contract without updating the game pot or enabling recovery, resulting in permanently stuck funds

Description

  • When players interact with the contract, all ETH sent should be accounted for within the game logic specifically, added to the pot or allocated properly ensuring all funds contribute to gameplay or are withdrawable by entitled participants.

  • The contract includes a receive() function that allows ETH to be sent directly to the contract, but these funds are not integrated into the game’s logic (e.g., not added to the pot) and there is no mechanism to withdraw them, effectively causing the ETH to become permanently locked.

// Root cause in the codebase with @> marks to highlight the relevant section
@> receive() external payable {}

Risk

Likelihood:

  • This will occur whenever a user mistakenly sends ETH directly to the contract address, either through the receive() function or by using a standard wallet transfer, instead of using the intended claimThrone() function.

  • Users interacting with the contract via scripts, bots, or wallets may assume all ETH sent will be properly processed or reflected in the game’s pot, especially if no clear documentation or validation prevents direct transfers.

Impact:

  • ETH sent directly to the contract is not added to the game’s pot, resulting in user funds being permanently locked and unusable within the game logic.

  • Accumulated ETH in the contract balance becomes inaccessible due to the absence of a withdrawal mechanism, increasing the protocol’s locked capital risk and reducing overall fund efficiency.

Proof of Concept


This test simulates a scenario where a player sends ETH directly to the contract using the receive() function. The ETH increases the contract’s balance but does not affect the in-game pot variable or any withdrawal mechanism, confirming that the funds are effectively stuck and inaccessible within the contract.

You can place this test in the Game.t.sol file:

function test_ReceivefunctiongetsFundsStuck() public {
vm.deal(player1, 10 ether);
assertEq(address(game).balance, 0);
assertEq(game.pot(), 0);
vm.prank(player1);
(bool success, ) = address(game).call{value: 5 ether}("");
assertTrue(success, "Failed to send Ether to the contract");
assertEq(address(game).balance, 5 ether);
assertEq(game.pot(), 0);
vm.prank(deployer);
vm.expectRevert();
(bool withdrawSuccess, ) = deployer.call{value: 5 ether}("");
assertFalse(withdrawSuccess, "Withdraw should fail as funds are stuck");
}

Recommended Mitigation


Note: To prevent accidental or unintended ETH transfers, implement a receive() function that reverts all direct ETH sends.

This ensures all ETH flows go through claimThrone() and protects users from losing funds via unintended transfers.

- receive() external payable {}
+ receive() external payable {
+ revert("Do not send ETH directly. Use claimThrone()");
+ }
Updates

Appeal created

inallhonesty Lead Judge about 2 months 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.