Last Man Standing

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

User's funds can stuck in the game contract if accidentally sent directly to the contract.

Root + Impact

Description

  • The game contract is designed for users to participate exclusively through the claimThrone function. This function correctly handles incoming funds by recording the deposit, registering the user as a player, and allocating the funds between the game owner and the eventual winner.

  • However, the contract also includes a receive function, which passively accepts plain ETH transfers. If a user accidentally sends ETH directly to the contract (e.g., via send, transfer, or a plain call), the receive function will accept the funds but will not add the amount to the game pot, register the sender as a player, or provide any way to recover the funds.

@> receive() external payable {}

Risk

Likelihood:

  • User will send claim fee using a smart contract or EAO with EIP-7702 via send, transfer, or a plain call

Impact:

  • Transfered ETH is not added to the game pot.

  • User is not registered as a player.

  • User's funds stuck forever in the game.

Proof of Concept

The following test should be added to the Game.t.sol file.

function test_FundsNotTrackedInGamePot() public {
// Deploy a new game
vm.prank(deployer);
game = new Game(
INITIAL_CLAIM_FEE,
GRACE_PERIOD,
FEE_INCREASE_PERCENTAGE,
PLATFORM_FEE_PERCENTAGE
);
// Check game's pot and total ETH balance before accidentally sending funds
console2.log("Game contract balance before: ", address(game).balance);
console2.log("Game contract pot before: ", game.pot());
// Pretend to be a user who accidentally send funds to the game contract using low level function
vm.prank(player2);
(bool success, ) = address(game).call{value: INITIAL_CLAIM_FEE * 5}("");
require(success);
// Check game's details after accidentally sending funds
console2.log("Game contract balance after: ", address(game).balance);
console2.log("Game contract pot after: ", game.pot());
console2.log("Current king is address : ", game.currentKing());
}

Recommended Mitigation

One way to handle this scenario is to call the claimThrone function inside the receive function to give a fair chance to become a king to unaware users.

- function claimThrone() external payable gameNotEnded nonReentrant {
+ function claimThrone() public payable gameNotEnded nonReentrant {
receive() external payable {
+ 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.