ProportionalToXPReward with N winners but the session strategy later returns a winners array of length M != N (e.g., because ties/draw expand beyond top N), every winner claim reverts with NumberOfWinnersMismatch. The session has already been Concluded, so the post-fee prize pool cannot be withdrawn and remains locked on SessionManager indefinitely.DefaultSession records all winners from UMA; at claim time ProportionalToXPReward reverts for every winner due to NumberOfWinnersMismatch.SessionManager.SessionManager.totalCollectedAmount * (BASIS_POINTS - (creatorFee + protocolFee + REFERRER_FEE)) / BASIS_POINTS (minus any fees already distributed). This can be the majority of sponsorship funds and/or entries.. It requires only normal creator configuration at Created time and an offchain winners computation that includes ties/draw of winers greater than configured `ProportionalToXPReward (or otherwise yields a different number of winners than the creator set). Onchain only checks that rewards are “configured” (>0), not that the configured count matches the final winners length.
src/reward/ProportionalToXPReward.solsrc/SessionManager.solsrc/session/DefaultSession.solrewardsConfigured(sessionId); for Proportional this means numberOfWinners > 0, not that it matches winners length:require(
IRewardStrategy(game.rewardStrategy).rewardsConfigured(sessionId),
RewardsNotConfigured(sessionId)
);
game.state = SessionState.Ongoing;
function rewardsConfigured(uint256 sessionId) external view returns (bool) {
return numberOfWinners[sessionId] > 0;
}
require(numberOfWinners[sessionId] == winners.length, NumberOfWinnersMismatch(sessionId, winners.length));
ISessionStrategy sessionStrategy = ISessionStrategy(sessionManager.getSessionStrategy(sessionId));
uint256[] memory xps = new uint256[](winners.length);
// ...
rewards[i] = xps[i] * prizePool / totalXP;
require(numberOfWinners[sessionId] == winners.length, NumberOfWinnersMismatch(sessionId, winners.length));
ISessionStrategy sessionStrategy = ISessionStrategy(sessionManager.getSessionStrategy(sessionId));
// ...
reward = userXP * prizePool / totalXP;
SessionManager delegates the calculation to the reward strategy; a mismatch thus DoS-es all claims:address[] memory winners = ISessionStrategy(game.sessionStrategy).getWinners(sessionId);
require(winners[position] == msg.sender, NotWinner(winners[position], msg.sender));
uint256 prizePool = getRewards(sessionId);
uint256 reward = IRewardStrategy(game.rewardStrategy).getReward(sessionId, winners, position, prizePool);
_distributeRewards(sessionId, msg.sender, reward);
function assertResults(
uint256 sessionId,
string calldata resultUri,
address[] calldata proposedWinners,
uint256[] calldata totalXPs,
uint256[] calldata totalTimes,
bytes[] calldata solutions
) external returns (bytes32 assertionId) {
// ... forwards full winners data to UMA
}
function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully) public override {
require(msg.sender == address(optimisticOracle), NotOptimisticOracle(msg.sender));
// ... on truthful resolution
recordResults(assertions[assertionId].sessionId, assertionId);
}
function recordResults(uint256 sessionId, bytes32 assertionId) public {
require(SessionManager(sessionManager).getSessionState(sessionId) == SessionState.Ended, GameNotEnded());
// ... set per-user results
winners[sessionId] = assertion.winners;
}
winners.length and XP totals actually returned by the session strategy.numberOfWinners to the actual winners length before any claim.sweepUnclaimed(sessionId) after a grace period to transfer stranded balances to a deterministic address (e.g., treasury or sponsor).Files added:
test/fuzz/TieExpansionWinnersMismatchFuzz.t.sol — strategy simulates ties between winners so winners = topN + 1; creator set topN, mismatch causes lockRun:
sol file attached due to size limit
Compiling 1 files with Solc 0.8.30
Solc 0.8.30 finished in 4.20s
Compiler run successful!
Ran 1 test for test/fuzz/TieExpansionWinnersMismatchFuzz.t.sol:TieExpansionWinnersMismatchFuzz
[PASS] testFuzz_TiesExpandWinners_MismatchLocks(uint128) (runs: 257, μ: 7673002, ~: 7673002)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 182.99ms (182.25ms CPU time)
Ran 1 test suite in 184.69ms (182.99ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)