After rescheduling, contestants can leave and be refunded, reducing numContestants below the configured number of winners. Reward strategies that require exact winners length (e.g., ProportionalToXPReward) or ranks preallocation can then become unclaimable or misallocated, leading to funds stuck.
Contestant can leave after reschedule and get refund while decrementing contestants:
function leaveRescheduledGame(uint256 sessionId)
external
onlyState(sessionId, SessionState.Created)
onlyJoined(sessionId)
{
Game storage game = games[sessionId];
require(game.originalStartTime != 0, GameIsNotRescheduled(sessionId));
require(game.originalStartTime + minimumRescheduleTime > block.timestamp, TooLateToLeave(sessionId));
contestants[sessionId][msg.sender] = false;
game.numContestants--;
_refundEntryFee(sessionId, msg.sender);
emit RefundRequested(sessionId, msg.sender);
}
Fixed ranks pay by configured slots; any non-existent rank’s share remains undistributed:
function getRewards(uint256 sessionId, address[] calldata winners, uint256 prizePool)
external
view
returns (uint256[] memory rewards)
{
require(rankedRewards[sessionId].length >= winners.length, GetRewardOutOfRange(sessionId));
rewards = new uint256[](winners.length);
for (uint256 i; i < winners.length; ++i) {
rewards[i] = prizePool * rankedRewards[sessionId][i] / BASIS_POINTS;
}
}
Note: With fewer winners than configured ranks, the leftover rank percentages have no recipient and remain in the contract.
numberOfWinners = 4 and configures fixed ranks: rankedRewards = [10, 10, 10, 10] (sum = 40, i.e., 40%).leaveRescheduledGame, leaving only 3 winners.1/4 of the intended distribution portion of the prize pool.rankedRewards = [0, 0, 0, 0, 10000] while only 3 winners will realistically exist.BASIS_POINTS:function setRankedRewards(uint256 sessionId, uint256[] calldata _rankedRewards) external {
require(sessionManager.getSessionState(sessionId) == SessionState.Created, NotCreated(sessionId));
require(sessionManager.getCreator(sessionId) == msg.sender, NotCreator(sessionId, msg.sender));
require(rankedRewards[sessionId].length == 0, AlreadySet(sessionId));
require(_rankedRewards.length > 0, InvalidRanks(sessionId, _rankedRewards.length));
require(_rankedRewards.length <= 20, InvalidRanks(sessionId, _rankedRewards.length));
uint256 totalPoints;
for (uint256 i; i < _rankedRewards.length; ++i) {
totalPoints += _rankedRewards[i];
}
require(totalPoints == BASIS_POINTS, InvalidTotalPoints(sessionId, totalPoints));
rankedRewards[sessionId] = _rankedRewards;
}
getRewards iterates only first 3 slots (indices 0..2) and pays 0 to all of them; the 10000 bps allocated to index 4 has no recipient and remains undistributed.require(rankedRewards[sessionId].length >= winners.length, GetRewardOutOfRange(sessionId));
rewards = new uint256[](winners.length);
for (uint256 i; i < winners.length; ++i) {
rewards[i] = prizePool * rankedRewards[sessionId][i] / BASIS_POINTS;
}
leaveRescheduledGame), so the creator can set these ranks during Created state after players join, trapping them into a zero-payout configuration.function leaveRescheduledGame(uint256 sessionId) external onlyState(sessionId, SessionState.Created) onlyJoined(sessionId) {
Game storage game = games[sessionId];
require(game.originalStartTime != 0, GameIsNotRescheduled(sessionId));
require(game.originalStartTime + minimumRescheduleTime > block.timestamp, TooLateToLeave(sessionId));
contestants[sessionId][msg.sender] = false;
game.numContestants--;
_refundEntryFee(sessionId, msg.sender);
}
numberOfWinners to be <= current numContestants at start, and re-validate before conclusion.min(configured, numContestants).winners.length < configured threshold.Will be made available on demand.