Last Man Standing

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

ETH Lost After Game Ends Due to Unrestricted receive()

Root + Impact

Description

  • Normal behavior:
    The contract should not accept ETH after the game has ended, or should immediately refund any ETH sent after game end, to prevent accidental loss of funds.

    Specific issue:
    The contract’s receive() function accepts ETH at any time, even after gameEnded == true. Any ETH sent to the contract after the game ends is not added to the pot or platform fees, and there is no mechanism to withdraw or recover these funds. This results in ETH being permanently locked in the contract.

// src/Game.sol
receive() external payable {}
// @> Vulnerability: Accepts ETH even after gameEnded == true, causing permanent loss of funds.

Risk

Likelihood:

  • This will occur whenever anyone sends ETH to the contract after the game ends.


Impact:

  • ETH is permanently lost and cannot be recovered by any user or the owner.

  • Users may lose funds by mistake, leading to loss of trust in the protocol.

Proof of Concept

The following test demonstrates the bug. After the game ends, a user sends ETH to the contract. The ETH is accepted, but is not added to the pot or platform fees, and cannot be withdrawn by anyone.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract GameC5LostETHTest is Test {
Game public game;
address public deployer;
address public player1;
address public stranger;
uint256 public constant INITIAL_CLAIM_FEE = 0.1 ether;
uint256 public constant GRACE_PERIOD = 1 days;
uint256 public constant FEE_INCREASE_PERCENTAGE = 10;
uint256 public constant PLATFORM_FEE_PERCENTAGE = 5;
function setUp() public {
deployer = makeAddr("deployer");
player1 = makeAddr("player1");
stranger = makeAddr("stranger");
vm.deal(deployer, 10 ether);
vm.deal(player1, 10 ether);
vm.deal(stranger, 10 ether);
vm.startPrank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
vm.stopPrank();
}
function testLostETHAfterGameEnds() public {
// Player1 claims throne
vm.startPrank(player1);
game.claimThrone{value: INITIAL_CLAIM_FEE}();
vm.stopPrank();
// Fast forward past grace period and end the game
vm.warp(block.timestamp + GRACE_PERIOD + 1);
game.declareWinner();
// Stranger sends ETH directly to contract after game ended
uint256 sendAmount = 1 ether;
vm.startPrank(stranger);
(bool sent, ) = address(game).call{value: sendAmount}("");
vm.stopPrank();
assertTrue(sent, "ETH transfer failed");
// The ETH is stuck: not in pot, not in platformFeesBalance, not withdrawable
assertEq(game.pot(), 0, "Pot should be zero after game ended");
assertEq(game.platformFeesBalance(), 0, "Platform fees should be zero after withdrawal");
assertEq(address(game).balance, sendAmount, "ETH is stuck in contract");
}
}

Recommended Mitigation

Block or refund ETH sent to the contract after the game ends by updating the receive() function:

- receive() external payable {}
+ receive() external payable {
+ require(!gameEnded, "Game: Cannot receive ETH after game ended.");
+ pot += msg.value;
+ }
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.

r4y4n3 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.