code-423n4 / 2024-07-traitforge-findings

2 stars 1 forks source link

Lack of mechanism to ensure all batches are initialized before entropy is used #1087

Closed howlbot-integration[bot] closed 2 months ago

howlbot-integration[bot] commented 2 months ago

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.

// 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
  }

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
  }

Assessed type

Other

c4-judge commented 2 months ago

koolexcrypto marked the issue as duplicate of #1086

c4-judge commented 2 months ago

koolexcrypto marked the issue as satisfactory

c4-judge commented 2 months ago

koolexcrypto changed the severity to 2 (Med Risk)