Identical modulo operations in both branches of transformRandomToPixel leads to biased lottery draws
transformRandomToPixel() executes the same modulo in both rejection-sampling branches, negating bias correction. Pixels 0-57,895 receive ~0.059% higher selection probability than pixels 57,896-98,279.
Description
The transformRandomToPixel function attempts to implement rejection sampling to ensure unbiased random number distribution but fails due to executing identical modulo operations in both the "biased" and "unbiased" branches. This completely negates the bias correction mechanism, resulting in a systematically unfair lottery system.
function transformRandomToPixel(uint256 randomNumber) public pure returns (uint32) {uint256 unbiasedRange = (type(uint256).max / 98_280) * 98_280;if (randomNumber >= unbiasedRange) {return uint32(randomNumber % 98_280); // Same operation as below}return uint32(randomNumber % 98_280); // Same operation as above}
The function calculates an unbiasedRange to identify numbers that would introduce bias, but then applies the same biased modulo operation (randomNumber % 98_280) regardless of which branch is taken. This makes the conditional check completely meaningless.
Since 2^256 is not perfectly divisible by 98_280, the remainder 2^256 % 98_280 = 57,896 means that pixel numbers 0-57,895 have a slightly higher probability of being selected than pixel numbers 57,896-98,279.
Vulnerable Scenario:
The following steps help understand the issue:
- VRF generates a random number in the "biased region" (>= unbiasedRange)
- Function detects this should be handled specially to avoid bias
- Instead of rejecting or resampling, it applies the same biased modulo operation
- Lower pixel numbers (0-57,895) get selected with ~0.059% higher probability
- Over time, certain lottery participants have systematically better odds
Impact
This vulnerability fundamentally compromises the lottery's integrity by creating an unfair distribution where certain pixel numbers have measurably higher winning probabilities. The bias violates the core fairness guarantee explicitly stated in the contract documentation and undermines user trust in the system. Over millions of draws, this creates a significant and measurable advantage for participants who purchase lower-numbered pixels, constituting a breach of the lottery's promise of equal odds for all participants.
Recommendation
Proper Rejection Sampling
function transformRandomToPixel(uint256 randomNumber) public pure returns (uint32) {uint256 unbiasedRange = (type(uint256).max / 98_280) * 98_280;if (randomNumber >= unbiasedRange) {// Use hash-based resampling for biased regionuint256 newRandom = uint256(keccak256(abi.encode(randomNumber)));return transformRandomToPixel(newRandom);}return uint32(randomNumber % 98_280);}
This will ensure mathematical correctness and maintain the original rejection sampling intent, ensuring accurate uniform distribution across all 98,280 possible pixel combinations.
Resolution
Golden Grid: Confirmed. We proposed a solution that differs from the proposed one to avoid infinite loops. Hence, restricting the loop to 5 retries.
Zealynx: Fixed. Agreed with the proposed solution.

