Status DataClose notification

Majority Games Disclosed Report

Incorrect Referrer Fee Deduction in getRewards

Created date
Aug 30 2025

Target

hidden

Vulnerability Details

Description

The getRewards function is responsible for calculating the total rewards available for distribution to winners by deducting protocol, creator, and referrer fees from the pool’s totalCollectedAmount. However, the current implementation applies the REFERRER_FEE deduction to the entire totalCollectedAmount, which includes both entry fees (from players) and sponsor contributions.

In reality, referrer rewards are only accumulated on entry fees, not on sponsor amounts. This discrepancy leads to an over-deduction whenever sponsor contributions are present.

Vulnerability Details

 /**
     * @notice Calculates the total rewards available for distribution to winners
     * @param sessionId The ID of the session
     * @return The amount of tokens available for rewards after deducting fees
     */
    function getRewards(uint256 sessionId) public view returns (uint256) {
        GamePool storage pool = gamePools[sessionId];
        return pool.totalCollectedAmount * (BASIS_POINTS - (pool.creatorFee + pool.protocolFee + REFERRER_FEE))
            / BASIS_POINTS;
    }

REFERRER_FEE is applied on totalCollectedAmount, while actual payout logic only considers entryFees.

This creates a mismatch: referrer fees are deducted from sponsor funds, but never actually paid out, leaving a portion of funds locked in the contract.

Impact

When sponsor contributions exist, part of the funds become permanently unclaimable.

2% of any amount added by the sponsors are always stuck in the contract.

Example:

Entry fees = 1000e18

Sponsors = 1000e18

Total = 2000e18

Referrer payouts = 1000e18 * 2% = 20e18

But getRewards deducts 2000e18 * 2% = 40e18

Mismatch = 20e18 tokens stuck in contract.

Validation steps

Copy test into test/SessionManagerEndGame.t.sol


function test_referral_funds_stuck() public {
        _createGame(sessionManager, promptHashes, promptStrategies, sessionStrategy, rewardStrategy, usdc);

        _addContestants();
        address sponsor1 = makeAddr("Sponsor1");
        address sponsor2 = makeAddr("Sponsor2");
        TestUSDC(usdc).mint(sponsor1, 100 ether);
        TestUSDC(usdc).mint(sponsor2, 100 ether);
        // First sponsor
        vm.startPrank(sponsor1);
        TestUSDC(usdc).approve(address(sessionManager), 100 ether);
        sessionManager.sponsorGame(1, 100 ether);
        vm.stopPrank();

        // Second sponsor
        vm.startPrank(sponsor2);
        TestUSDC(usdc).approve(address(sessionManager), 100 ether);
        sessionManager.sponsorGame(1, 100 ether);
        vm.stopPrank();

        vm.prank(address(this));
        _startGame();

        _revealQuestion();
        _warpToEndTime();
        sessionManager.endGame(1);

        _concludeGame();
        sessionManager.distributeFees(1);

        for (uint256 i = 0; i < 3; i++) {
            vm.startPrank(contestants[i]);
            sessionManager.claimRewards(1, i);
            vm.stopPrank();
        }

        vm.startPrank(address(this));
        sessionManager.claimReferralRewards(Solarray.uint256s(1));
        vm.stopPrank();

        assertGt(TestUSDC(usdc).balanceOf(address(sessionManager)), 0);
        assertEq(TestUSDC(usdc).balanceOf(address(sessionManager)), 4e18); // 4 USDC stuck in contract
    }


Attachments

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