The EntropyGenerator contract initializes entropy values in batches to manage gas costs. However, there is no mechanism to ensure that all batches are fully initialized before entropy values are used. This could lead to scenarios where uninitialized entropy slots are accessed, returning default zero values.
Proof of Concept
The contract's functions writeEntropyBatch1(), writeEntropyBatch2(), and writeEntropyBatch3() initialize entropy slots incrementally.
// Functions to initalize entropy values inbatches to spread gas cost over multiple transcations
function writeEntropyBatch1() public {
require(lastInitializedIndex < batchSize1, 'Batch 1 already initialized.');
uint256 endIndex = lastInitializedIndex + batchSize1; // calculate the end index for the batch
unchecked {
for (uint256 i = lastInitializedIndex; i < endIndex; i++) {
uint256 pseudoRandomValue = uint256(
keccak256(abi.encodePacked(block.number, i))
) % uint256(10) ** 78; // generate a pseudo-random value using block number and index
require(pseudoRandomValue != 999999, 'Invalid value, retry.');
entropySlots[i] = pseudoRandomValue; // store the value in the slots array
}
}
lastInitializedIndex = endIndex;
}
// second batch initialization
function writeEntropyBatch2() public {
require(
lastInitializedIndex >= batchSize1 && lastInitializedIndex < batchSize2,
'Batch 2 not ready or already initialized.'
);
uint256 endIndex = lastInitializedIndex + batchSize1;
unchecked {
for (uint256 i = lastInitializedIndex; i < endIndex; i++) {
uint256 pseudoRandomValue = uint256(
keccak256(abi.encodePacked(block.number, i))
) % uint256(10) ** 78;
require(pseudoRandomValue != 999999, 'Invalid value, retry.');
entropySlots[i] = pseudoRandomValue;
}
}
lastInitializedIndex = endIndex;
}
// allows setting a specific entropy slot with a value
function writeEntropyBatch3() public {
require(
lastInitializedIndex >= batchSize2 && lastInitializedIndex < maxSlotIndex,
'Batch 3 not ready or already completed.'
);
unchecked {
for (uint256 i = lastInitializedIndex; i < maxSlotIndex; i++) {
uint256 pseudoRandomValue = uint256(
keccak256(abi.encodePacked(block.number, i))
) % uint256(10) ** 78;
entropySlots[i] = pseudoRandomValue;
}
}
lastInitializedIndex = maxSlotIndex;
}
However, the getNextEntropy() and getEntropy() functions do not verify whether all batches are fully initialized before using the entropy slots.
// function to retrieve the next entropy value, accessible only by the allowed caller
function getNextEntropy() public onlyAllowedCaller returns (uint256) {
require(currentSlotIndex <= maxSlotIndex, 'Max slot index reached.');
uint256 entropy = getEntropy(currentSlotIndex, currentNumberIndex);
if (currentNumberIndex >= maxNumberIndex - 1) {
currentNumberIndex = 0;
if (currentSlotIndex >= maxSlotIndex - 1) {
currentSlotIndex = 0;
} else {
currentSlotIndex++;
}
} else {
currentNumberIndex++;
}
// Emit the event with the retrieved entropy value
emit EntropyRetrieved(entropy);
return entropy;
}
// private function to calculate the entropy value based on slot and number index
function getEntropy(
uint256 slotIndex,
uint256 numberIndex
) private view returns (uint256) {
require(slotIndex <= maxSlotIndex, 'Slot index out of bounds.');
if (
slotIndex == slotIndexSelectionPoint &&
numberIndex == numberIndexSelectionPoint
) {
return 999999;
}
uint256 position = numberIndex * 6; // calculate the position for slicing the entropy value
require(position <= 72, 'Position calculation error');
uint256 slotValue = entropySlots[slotIndex]; // slice the required [art of the entropy value
uint256 entropy = (slotValue / (10 ** (72 - position))) % 1000000; // adjust the entropy value based on the number of digits
uint256 paddedEntropy = entropy * (10 ** (6 - numberOfDigits(entropy)));
return paddedEntropy; // return the caculated entropy value
}
An attacker calls getNextEntropy() before all batches are fully initialized.
The attacker receives predictable zero values, allowing them to exploit the system.
Accessing uninitialized entropy slots can return predictable zero values, undermining the intended randomness and fairness of the system.
Tools Used
Manual Review
Recommended Mitigation Steps
Add a check to ensure that all batches are fully initialized before allowing entropy retrieval. For example, include the following check in the getNextEntropy() and getEntropy() functions:
// function to retrieve the next entropy value, accessible only by the allowed caller
function getNextEntropy() public onlyAllowedCaller returns (uint256) {
require(currentSlotIndex <= maxSlotIndex, 'Max slot index reached.');
+ require(lastInitializedIndex == maxSlotIndex, "Entropy slots not fully initialized.");
uint256 entropy = getEntropy(currentSlotIndex, currentNumberIndex);
if (currentNumberIndex >= maxNumberIndex - 1) {
currentNumberIndex = 0;
if (currentSlotIndex >= maxSlotIndex - 1) {
currentSlotIndex = 0;
} else {
currentSlotIndex++;
}
} else {
currentNumberIndex++;
}
// Emit the event with the retrieved entropy value
emit EntropyRetrieved(entropy);
return entropy;
}
// private function to calculate the entropy value based on slot and number index
function getEntropy(
uint256 slotIndex,
uint256 numberIndex
) private view returns (uint256) {
require(slotIndex <= maxSlotIndex, 'Slot index out of bounds.');
+ require(lastInitializedIndex == maxSlotIndex, "Entropy slots not fully initialized.");
if (
slotIndex == slotIndexSelectionPoint &&
numberIndex == numberIndexSelectionPoint
) {
return 999999;
}
uint256 position = numberIndex * 6; // calculate the position for slicing the entropy value
require(position <= 72, 'Position calculation error');
uint256 slotValue = entropySlots[slotIndex]; // slice the required [art of the entropy value
uint256 entropy = (slotValue / (10 ** (72 - position))) % 1000000; // adjust the entropy value based on the number of digits
uint256 paddedEntropy = entropy * (10 ** (6 - numberOfDigits(entropy)));
return paddedEntropy; // return the caculated entropy value
}
Lines of code
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntropyGenerator/EntropyGenerator.sol#L101 https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntropyGenerator/EntropyGenerator.sol#L164
Vulnerability details
Impact
The EntropyGenerator contract initializes entropy values in batches to manage gas costs. However, there is no mechanism to ensure that all batches are fully initialized before entropy values are used. This could lead to scenarios where uninitialized entropy slots are accessed, returning default zero values.
Proof of Concept
The contract's functions writeEntropyBatch1(), writeEntropyBatch2(), and writeEntropyBatch3() initialize entropy slots incrementally.
However, the getNextEntropy() and getEntropy() functions do not verify whether all batches are fully initialized before using the entropy slots.
Accessing uninitialized entropy slots can return predictable zero values, undermining the intended randomness and fairness of the system.
Tools Used
Manual Review
Recommended Mitigation Steps
Add a check to ensure that all batches are fully initialized before allowing entropy retrieval. For example, include the following check in the getNextEntropy() and getEntropy() functions:
Assessed type
Other