F-2025-0022·fairness-flaw

Raffle winner can also win instant rewards, which will give them more than 50% of the expected allocation

Fixedrafflelotteryvrf
TL;DR

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.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
LOW
02Section · Description

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:

  1. A raffle round completes with 100 tickets sold, collecting 100 USDT total.
  2. The VRF callback in fulfillRandomWords() selects ticket #42 as the winning ticket, owned by Alice.
  3. Alice is set as the main winner and receives the main prize: (100 * 5000) / 10000 = 50 USDT.
  4. The _distributeInstantRewardsVRF() function is called to distribute instant rewards worth (100 * 1000) / 10000 = 10 USDT among up to 10 winners.
  5. 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)
  6. If offset = 42 or any of the calculated positions equals 42, Alice's ticket #42 is selected again as an instant winner.
  7. 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.

03Section · Recommendation

Recommendation

Modify _distributeInstantRewardsVRF() to exclude the main winner from instant reward selection:

solidity
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 ticket
if (ticketIndex == mainWinningTicket) continue;
address instantWinner = ticketOwner[productId][roundId][ticketIndex];
if (instantWinner != address(0)) {
_safeTransfer(instantWinner, prizePerWinner);
}
}
}
04Section · Resolution

Resolution

Nexalo: Fixed.

Zealynx: Verified. Fixed.

Status
Fixed
F-2025-0022

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx