Based on the contract logic, the raffle works by sampling a value $ {x \in \mathbb{Z} | 0 \leq x \lt 2^{256} } $ (aka uint256) from a uniform distribution (provided by Chainlink VRF), then applying modulo tokenRange, generating a new value $ {y \in \mathbb{Z} | 0 \leq y \lt tokenRange } $. Consider Y the probability mass function of $ y $, then this raffle can be considered fair only if all the values $y$ in tokenRange have the same probability of being chosen ($ Pr(Y = y) = \frac{1}{tokenRange} $). But this is only true if tokenRange is a power of 2. If tokenRange is not a power of 2, then not all possible values of $y$ have the same probability and it can be shown that the relative difference in probability between the value $y$ with the highest probability and the value $y$ with the lowest probability is $ \approx \frac{tokenRange}{2^{256}} $. Therefore for most tokenRange this can be considered negligible, although still theoretically an unfair raffle.
The ERC721 Standart does not set a upper limit on the number of tokens, so in some edge cases where tokenRange can be chosen to be arbitrarily large this difference in probabilities can become a real issue.
Recommended Mitigation Steps
Please consider checking if tokenRange is a power of 2. So the raffle can be considered fair no matter how large tokenRange is.
diff --git a/VRFNFTRandomDraw.sol.orig b/VRFNFTRandomDraw.sol
index 668bc56..333a0ff 100644
--- a/VRFNFTRandomDraw.sol.orig
+++ b/VRFNFTRandomDraw.sol
@@ -111,7 +111,8 @@ contract VRFNFTRandomDraw is
// and the size of the range needs to be at least 2 (end is exclusive)
if (
_settings.drawingTokenEndId < _settings.drawingTokenStartId ||
- _settings.drawingTokenEndId - _settings.drawingTokenStartId < 2
+ _settings.drawingTokenEndId - _settings.drawingTokenStartId < 2 ||
+ (_settings.drawingTokenEndId - _settings.drawingTokenStartId) % 2 == 0
) {
revert DRAWING_TOKEN_RANGE_INVALID();
}
And also modify the tests to conform to this new check.
Lines of code
https://github.com/code-423n4/2022-12-forgeries/blob/fc271cf20c05ce857d967728edfb368c58881d85/src/VRFNFTRandomDraw.sol#L254-L256
Vulnerability details
Impact
Based on the contract logic, the raffle works by sampling a value $ {x \in \mathbb{Z} | 0 \leq x \lt 2^{256} } $ (aka
uint256
) from a uniform distribution (provided by Chainlink VRF), then applying modulotokenRange
, generating a new value $ {y \in \mathbb{Z} | 0 \leq y \lt tokenRange } $. Consider Y the probability mass function of $ y $, then this raffle can be considered fair only if all the values $y$ intokenRange
have the same probability of being chosen ($ Pr(Y = y) = \frac{1}{tokenRange} $). But this is only true iftokenRange
is a power of 2. IftokenRange
is not a power of 2, then not all possible values of $y$ have the same probability and it can be shown that the relative difference in probability between the value $y$ with the highest probability and the value $y$ with the lowest probability is $ \approx \frac{tokenRange}{2^{256}} $. Therefore for mosttokenRange
this can be considered negligible, although still theoretically an unfair raffle.The ERC721 Standart does not set a upper limit on the number of tokens, so in some edge cases where
tokenRange
can be chosen to be arbitrarily large this difference in probabilities can become a real issue.Recommended Mitigation Steps
Please consider checking if
tokenRange
is a power of 2. So the raffle can be considered fair no matter how largetokenRange
is.And also modify the tests to conform to this new check.