Status DataClose notification

Majority Games Disclosed Report

Incomplete reward distribution in `FixedRanksReward` leaves part of the prize pool permanently locked

Created date
Aug 30 2025

Target

hidden

Vulnerability Details

Description: The FixedRanksReward contract distributes rewards based on predefined ranks (rankedRewards). The current check condition allows the number of winners to be less than the number of ranks set by the session creator:

require(rankedRewards[sessionId].length >= winners.length, GetRewardOutOfRange(sessionId));

This means the system only ensures that there are not more winners than available ranks, but it does not enforce that the number of winners matches the number of ranks.

Since rankedRewards in FixedRanksReward contract's setRankedRewards function is designed such that all its elements together make up 100% of the prize pool (a full BasisPoint):

        uint256 totalPoints;
        for (uint256 i; i < _rankedRewards.length; ++i) {
            totalPoints += _rankedRewards[i];
        }

@>        require(totalPoints == BASIS_POINTS, InvalidTotalPoints(sessionId, totalPoints));

Any mismatch results in incomplete allocation. If even one rankedRewards element remains unused (because fewer winners are provided), part of the prize pool remains undistributed.

A practical case occurs when a session start date is extended by the creator and some participants leave, reducing the final number of winners below the configured ranks. As a result, a portion of the prize pool remains stuck in the contract with no mechanism to recover or reallocate it.

Impact:

  • Users: Winners receive less than the total prize pool intended for distribution.
  • Protocol: Funds accumulate in the contract without a retrieval path, leading to permanent locked funds.
  • Severity: Critical, as it leads to permanent fund loss and breaks the intended reward mechanism.

Mitigation: Enforce that rankedRewards[sessionId].length == winners.length to ensure complete distribution.

Validation steps

Attack Scenario:

  1. Session creator sets rankedRewards with, e.g., 5 ranks (100%, distributed as portions).
  2. Session concludes, but only 3 participants remain as winners.
  3. The check passes since rankedRewards.length (5) >= winners.length (3).
  4. Rewards are distributed only across 3 winners, leaving the portions of ranks 4 and 5 unallocated.
  5. These unallocated funds remain permanently locked in the contract.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Critical
Bounty$669
Visibilitypartially
VulnerabilityOther
Participants
hidden