Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Invalid

withdrawFees Creates a Single Point of Failure

Summary

In the RockPaperScissors contract, the withdrawFees function allows only the adminAddress to withdraw accumulated protocol fees. However, if the admin address becomes inaccessible (e.g., private key lost, set to a non-payable contract, or blackholed), the funds will be permanently locked within the contract. There is no fallback or emergency mechanism to recover these funds, making this a critical single point of failure.

Vulnerability Details

The vulnerable code resides in:

function withdrawFees(uint256 _amount) external {
require(msg.sender == adminAddress, "Only admin can withdraw fees");
...
(bool success,) = adminAddress.call{value: amountToWithdraw}("");
require(success, "Fee withdrawal failed");
}

The issues here include:

  • Only adminAddress can call this function.

  • Funds are sent to adminAddress, which might be:

    • A non-payable contract

    • An EOA with lost private key

    • A misconfigured or unintended recipient

If the transfer fails, it reverts and the ETH remains stuck. There is no backup process to:

  • Change the admin through consensus or multi-sig,

  • Let another party recover fees,

  • Or automatically detect an invalid admin.

Impact

  • Permanent loss of protocol revenue: ETH fees become irretrievable.

  • Centralized failure point: Entire fee system depends on a single address.

  • Protocol sustainability risk: Admin cannot fund operations or maintenance if fees are locked.

  • No recourse for recovery: Users or developers cannot recover fees even in good faith.

Tools Used

  • Manual contract review

  • Ethereum transfer semantics

  • Solidity call{value:} behavior

  • Real-world examples (e.g., OpenZeppelin forum)

Recommendations

  1. Use a multi-signature wallet as the adminAddress to reduce the risk of inaccessibility.

    • Example: Gnosis Safe

  2. Add a safety valve for admin rotation, such as:

    function rescueFees(address payable newRecipient) external onlyOwner {
    require(newRecipient != address(0), "Invalid recipient");
    (bool success,) = newRecipient.call{value: address(this).balance}("");
    require(success, "Rescue failed");
    }
  3. Implement an admin recovery process, such as:

    • A DAO-controlled proposal to reassign admin

    • A trusted delay mechanism (e.g., if admin inactive for X days)

  4. Improve error handling:

    • Log failed transfers

    • Allow retries or secondary claimants

Updates

Appeal created

m3dython Lead Judge 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.