Last Man Standing

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

Direct ETH Transfers Lock Funds Outside `pot` Accounting

Direct ETH Transfers Lock Funds Outside pot Accounting

Description

  • Expected behaviour: All value held by the contract should be claimable by someone—either the current king (via pot) or the owner (via platform fees). Sending plain ETH to the contract should increase pot or be rejected.

  • Actual issue: The contract implements a bare receive() function:

receive() external payable {}

ETH sent with no calldata is accepted, but no state variable is updated. The value sits in address(this).balance yet is excluded from:

  • pot — winnings for the eventual king

  • platformFeesBalance — owner withdrawals

When declareWinner() is later called, only pot is moved to pendingWinnings; the stray ETH remains trapped forever.

Risk

Likelihood:

  • Anyone (or any bot) can send ETH to the contract at zero gas per call. It might even happen accidentally (e.g. mistaken address). Thus occurrence is high over time.

Impact:

  • The contract’s on-chain balance diverges from what off-chain UIs display as the pot. Users could believe more ETH is up for grabs than is actually winnable.

  • Locked funds lower the effective APY of the game and represent permanent value loss—griefers burn ETH to reduce everybody’s incentive.

Proof of Concept

The test below sends 1 wei directly to the contract, then shows that pot, platformFeesBalance, and pendingWinnings remain zero while the contract balance increases.

// Run with: forge test --match-contract PoCLockedEth -vvv
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {Game} from "../src/Game.sol";
contract PoCLockedEth is Test {
Game game;
function setUp() public {
game = new Game(0.1 ether, 1 days, 10, 5);
}
function test_DirectSendLocksEth() public {
// Direct send 1 wei to contract
payable(address(game)).transfer(1);
assertEq(address(game).balance, 1, "ETH reached contract");
assertEq(game.pot(), 0, "Pot did not increase");
assertEq(game.platformFeesBalance(), 0, "Platform fees did not increase");
}
}

Recommended Mitigation

Two safe options:

- receive() external payable {}
+
+ /// @dev Credit direct ETH to the pot so it becomes withdrawable by the winner.
+ receive() external payable {
+ pot += msg.value;
+ }

or, if direct sends are undesired:

- receive() external payable {}
+
+ /// @dev Disallow accidental or malicious direct transfers.
+ receive() external payable {
+ revert("Direct ETH not accepted; use claimThrone");
+ }

Either change prevents value from becoming permanently stranded.

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.

Support

FAQs

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