Raffle winner can also win instant rewards, which will give them more than 50% of the expected allocation
The instant-reward selection algorithm does not exclude the main winner ticket, so a single user can collect the main prize and additional instant rewards in the same round.
Description
During the reward distribution process in _distributeRound(), the main raffle winner can also be selected as one of the instant reward winners. This allows a single user to win both the main prize (50% of the pot) and one or more instant rewards (up to 10% of the pot), which contradicts the intended fairness of distributing rewards across multiple participants.
The issue occurs because _distributeInstantRewardsVRF() selects instant winners using a deterministic algorithm based on the same VRF random word used to select the main winner, but it does not exclude the main winner's ticket from the instant winner selection.
Vulnerable Scenario:
The following steps help understand the issue:
- A raffle round completes with 100 tickets sold, collecting 100 USDT total.
- The VRF callback in
fulfillRandomWords()selects ticket #42 as the winning ticket, owned by Alice. - Alice is set as the main winner and receives the main prize: (100 * 5000) / 10000 = 50 USDT.
- The
_distributeInstantRewardsVRF()function is called to distribute instant rewards worth (100 * 1000) / 10000 = 10 USDT among up to 10 winners. - The function calculates instant winner tickets using:
- seed = keccak256(randomWord, totalCollected, productId, roundId)
- offset = seed % 100
- step = 100 / 10 = 10
- Instant winners at tickets: offset, offset+10, offset+20, ..., offset+90 (all mod 100)
- If offset = 42 or any of the calculated positions equals 42, Alice's ticket #42 is selected again as an instant winner.
- Alice receives an additional 10 USDT / 10 = 1 USDT instant reward on top of her 50 USDT main prize.
The main raffle winner can receive additional instant rewards, concentrating more winnings in a single participant rather than distributing them fairly across multiple users. This undermines the purpose of instant rewards, which should provide smaller prizes to additional participants to increase engagement and perceived fairness. Users who could have won instant rewards lose their opportunity when the main winner occupies one of the instant winner slots.
Recommendation
Modify _distributeInstantRewardsVRF() to exclude the main winner from instant reward selection:
function _distributeInstantRewardsVRF(uint256 productId,uint256 roundId,uint256 totalAmount,uint256 randomWord) private {Round storage round = rounds[productId][roundId];if (round.ticketsSold == 0 || totalAmount == 0) return;uint256 winnersCount = round.ticketsSold < 10 ? round.ticketsSold : 10;uint256 prizePerWinner = totalAmount / winnersCount;uint256 seed = uint256(keccak256(abi.encodePacked(randomWord,round.totalCollected,productId,roundId)));uint256 mainWinningTicket = randomWord % round.ticketsSold;uint256 offset = seed % round.ticketsSold;uint256 step = round.ticketsSold / winnersCount;for (uint256 i = 0; i < winnersCount; i++) {uint256 ticketIndex = (offset + (i * step)) % round.ticketsSold;// Skip main winner's ticketif (ticketIndex == mainWinningTicket) continue;address instantWinner = ticketOwner[productId][roundId][ticketIndex];if (instantWinner != address(0)) {_safeTransfer(instantWinner, prizePerWinner);}}}
Resolution
Nexalo: Fixed.
Zealynx: Verified. Fixed.

