F-2025-0004·biased-randomness

Identical modulo operations in both branches of transformRandomToPixel leads to biased lottery draws

Fixedlotterypixel-lotterychainlink-vrf
TL;DR

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.

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

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.

solidity
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:

  1. VRF generates a random number in the "biased region" (>= unbiasedRange)
  2. Function detects this should be handled specially to avoid bias
  3. Instead of rejecting or resampling, it applies the same biased modulo operation
  4. Lower pixel numbers (0-57,895) get selected with ~0.059% higher probability
  5. Over time, certain lottery participants have systematically better odds
03Section · Impact

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.

04Section · Recommendation

Recommendation

Proper Rejection Sampling

solidity
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 region
uint256 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.

05Section · Resolution

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.

Status
Fixed
F-2025-0004

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx