Last Man Standing

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

Lack of Mechanism to Reclaim Abandoned Winnings

Root + Impact

Description

In the current game logic, when a new player claims the throne, the previous king is owed their ETH reward via a pull-payment mechanism (withdrawWinnings). This requires the former king to manually call a function to retrieve their ETH.

However, if the previous player never claims their funds (e.g., lost keys, inactive user, malicious address), the winnings remain indefinitely locked within the contract. Over time, this results in accumulation of idle ETH, which cannot be redistributed or recycled into gameplay.

// Root cause: No expiration or reclaim mechanism for unclaimed winnings
function withdrawWinnings() external {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWinnings[msg.sender] = 0;
payable(msg.sender).transfer(amount); @> No reclaim logic if never called
}

Risk

Likelihood Medium

  • Over time, it's realistic that some users will lose access to wallets or never return to claim rewards.

  • Especially in a long-running game, stale balances will accumulate from inactive addresses.

Impact Medium

  • Locked ETH permanently reduces circulating funds in the protocol.

  • Creates misleading balance states and may reduce future reward fairness.

  • No way to recycle abandoned winnings unless governance or upgrade is introduced later.


Proof of Concept

// Simulated Scenario:
// 1. Player A claims the throne and is owed 5 ETH after being dethroned.
// 2. Player A loses access to wallet or never calls withdrawWinnings()
// 3. 5 ETH remains in pendingWinnings[A] forever.
assert(pendingWinnings[playerA] == 5 ether); // True
// But:
vm.prank(playerA);
withdrawWinnings(); // never happens
// Admin or contract has no method to recover or redistribute these funds.

This can be tested in Foundry by simulating an address claiming the throne, getting dethroned, and never calling withdrawWinnings().


Recommended Mitigation

Introduce a timestamp tracking system for pending winnings, and allow the contract owner (or DAO/governance) to reclaim abandoned funds after a long delay (e.g., 365 days):

mapping(address => uint256) public pendingWinnings;
+ mapping(address => uint256) public lastClaimableTimestamp;
+ uint256 public constant RECLAIM_DELAY = 365 days;
+ function recordWinnings(address previousKing, uint256 amount) internal {
+ pendingWinnings[previousKing] += amount;
+ lastClaimableTimestamp[previousKing] = block.timestamp;
+ }
function withdrawWinnings() external {
uint256 amount = pendingWinnings[msg.sender];
require(amount > 0, "Nothing to withdraw");
pendingWinnings[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
+ function reclaimAbandonedWinnings(address user) external onlyOwner {
+ require(block.timestamp > lastClaimableTimestamp[user] + RECLAIM_DELAY, "Too early");
+ uint256 amount = pendingWinnings[user];
+ require(amount > 0, "No winnings to reclaim");
+ pendingWinnings[user] = 0;
+ payable(owner).transfer(amount); // Optionally redirect to treasury
+ }

Note-----

  • Emit events like ReclaimedWinnings(address user, uint256 amount) for full transparency.

  • This mechanism should be used sparingly and only after long delays.

  • In trustless systems, this logic could instead be governed by a DAO vote or community multisig to avoid centralization risk.


Updates

Appeal created

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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