xanoutas/solidity-security-findings huggingface dataset:
Lack of mechanism to ensure all batches are initialized before entropy is used
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# 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. 

```solidity
// 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.

```solidity
// function to retrieve the next entropy
Potential Uninitialized `entropySlots` Reading in `getNextEntropy`, Causing 0 Entropy Mint
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntropyGenerator/EntropyGenerator.sol#L103


# Vulnerability details

## Title

Potential Uninitialized `entropySlots` Reading in `getNextEntropy`

## Impact

The `getNextEntropy` function can be called at any time without waiting for the write entropy batches process to finish. This could lead to the function returning an uninitialized entropy value of `000000`, resulting in users losing funds to mint useless tokens and not being eligible for future airdrops as they get 0 shares. This vulnerability can severely impact the users' trust and the protocol's functionality.

## Proof of Concept

- Found in contracts/EntropyGenerator/EntropyGenerator.sol at [Line 103](https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntropyGenerator/EntropyGenerator.sol#L103)
> @>: if `currentSlotIndex > lastInitializedIndex`  (writeEntropyBatch process not complete), `getEntropy` would return entropy = 000000 instead of revert. Which leads to user mint entities with 0 entropy.

```solidity

101:  function getNextEntropy() public onlyAllowedCaller returns (uint256) { 
102:    require(currentSlotIndex <= maxSlotIndex, 'Max slot index reached.');
103:@>     uint256 entropy = getEntropy(currentSlotIndex, currentNumberIndex); 
104:
    ...
120:  }
```

### POC

Apply following POC via `git apply POC.patch` and run `yarn test`. The test confirms `getNextEntropy` did return entropy 0 instead of revert.

```patch
diff --git a/test/EntropyGenerator.test.ts b/test/EntropyGenerator.test.ts
index 69551f5..f7e8698 100644
--- a/test/EntropyGenerator.test.ts
+++ b/test/EntropyGenerator.test.ts
@@ -3,7 +3,7 @@ import { ethers } from 'hardhat';
 import { EntropyGenerator } from '../typechain-types';
 import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
 
-describe('EntropyGenerator', function () {
+describe.only('EntropyGenerator', function () {
   let entropyGenerator: EntropyGenerator;
   let owner: HardhatEthersSigner;
   let allowedCaller: HardhatEthersSigner;
@@ -32,6 +32,15 @@ describe('EntropyGenerator', function () {
     expect(updatedCaller).to.equal(allowedCaller);
   });
 
+  it('getNextEntropy returns 0 entropy if entropySlots not properly init', async function () {
+    // call getNextEntropy when write entropy batches not finished
+    await expect(
+      entropyGenerator.connect(allowedCaller).getNextEntropy()
+    ).to.emit(entropyGenerator, 'EntropyRetrieved').withArgs(0);
+
+    // @audit: should revert getNextEntropy if entropySlots not properly init
+  });
+
   it('should write entropy batches 1', async function () {
     // Write entropy batch 1
     await entropyGenerator.writeEntropyBatch1();
```

## Tools Used

Hardhat

## Recommended Mitigation Steps

Only allow `getNextEntropy` call if `currentSlotIndex < lastInitializedIndex` to ensure that the entropy slots are properly initialized:

```patch
  function getNextEntropy() publi
NFT Token cannot be `nuked` even if `minimumDaysHeld` period has been passed.
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L184
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L193
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L367
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L378


# Vulnerability details

## Impact
- Even when `minimumDaysHeld` period has been passed from the time it got minted user would not be able `nuke` NFT .
- This happens due to 2 reasons:
  - Let's suppose `minimumDaysHeld` has been completed and after that the user 
    puts NFT for sale using `listNFTForSale()`.
  - Inside `listNFTForSale()` the NFT get's transferred from user address to 
    contract address and during 
    `nftContract.transferFrom(msg.sender, address(this), tokenId);`
    transferFrom() , `_beforeTokenTransfer()` gets triggered which updates 
    `lastTokenTransferredTimestamp[firstTokenId] = block.timestamp;` to current 
    timestamp.
  - Now after sometime user cancels listing of NFT through `cancelListing()`.
  - Now when user tries to `nuke()` NFT at that time error would be thrown due to 
    this check
    `uint256 tokenAgeInSeconds = block.timestamp -
      nftContract.getTokenLastTransferredTimestamp(tokenId);
    return tokenAgeInSeconds >= minimumDaysHeld;
    `
  - As `lastTokenTransferredTimestamp` gets updated during `beforeTransfer()` so it would be less than `minimumDaysHeld` period which leads to users not able to `nuke()` NFT.

    

## Proof of Concept
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L184
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L193
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L367
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L378

## Tools Used
Manual Review
## Recommended Mitigation Steps
- The check should be such that even though he interracts with Trading contract but he should be able to `nuke` NFT as user has passed minimum days period.


## Assessed type

Other
User cannot nuke NFT after `minimumDaysHeld` if user `listNFTForSale` and `cancelListing` of NFT
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L367-L392
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L184-L194
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/NukeFund/NukeFund.sol#L163
https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/EntityTrading/EntityTrading.sol#L53


# Vulnerability details

## Impact
- User can `nuke` NFT after holding it more than `minimumDaysHeld` period which is set as 3 days.
- It is checked by `canTokenBeNuked` function where it checks `uint256 tokenAgeInSeconds = block.timestamp -
      nftContract.getTokenLastTransferredTimestamp(tokenId)`.
- But when user list NFT for sale by `listNFTForSale` and then cancel listing by `cancelListing` function, in that `transferFrom` function is called and in which `_beforeTokenTransfer` is called.
- Now, in `_beforeTokenTransfer`, this mapping is updated to block.timestamp `lastTokenTransferredTimestamp[firstTokenId] = block.timestamp`.
- After this if user want to `nuke` NFT then user has to wait again for 3 days because mapping `lastTokenTransferredTimestamp` is reset to block.timestamp.

## Proof of Concept
```solidity
function canTokenBeNuked(uint256 tokenId) public view returns (bool) {
    // Ensure the token exists
    require(
      nftContract.ownerOf(tokenId) != address(0),
      'ERC721: operator query for nonexistent token'
    );
    uint256 tokenAgeInSeconds = block.timestamp -
@>      nftContract.getTokenLastTransferredTimestamp(tokenId);
    // Assuming tokenAgeInSeconds is the age of the token since it's holding the nft, check if it's over minimum days held
    return tokenAgeInSeconds >= minimumDaysHeld;
  }
```
```solidity
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 firstTokenId,
    uint256 batchSize
  ) internal virtual override {
    super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

    uint listedId = entityForgingContract.getListedTokenIds(firstTokenId);
    /// @dev don't update the transferred timestamp if from and to address are same
    if (from != to) {
      // q updated here so can not nuke even after 3 days?
@>      lastTokenTransferredTimestamp[firstTokenId] = block.timestamp;
    }

    if (listedId > 0) {
      IEntityForging.Listing memory listing = entityForgingContract.getListings(
        listedId
      );
      if (
        listing.tokenId == firstTokenId &&
        listing.account == from &&
        listing.isListed
      ) {
        entityForgingContract.cancelListingForForging(firstTokenId);
      }
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
- Protocol has to make sure that if user list NFT for sale and then cancel it and now if user wants to nuke the NFT then user does not need to wait again for `minimumDaysHeld`.
- User can be nuke NFT after `minimumDaysHeld` period passed after minting NFT.


## Assessed t
Title Users can be temporary DOSed and unable to `nuke`
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L182


# Vulnerability details

## Summary

The `nuke` function which allows users to Nuke their NFTs strategically in order to claim ETH based on the Entity’s nuke factor can be temporarily DOSed or breaks the contracts logic

## Description

The `nuke` function is meant to allow players to "nuke" (destroy) their Entity in exchange for ETH. However, according the logic and implementation of the contract, users should only be able to nuke when a Token has been matured i.e the token Last Transferred Timestamp is greater than than the minimum Days in which the token has been Held for and This is strictly checked with the following code logic:

- `require(canTokenBeNuked(tokenId), 'Token is not mature yet');`

And the `canTokenBeNuked` is implemented as follows

```solidity
    function canTokenBeNuked(uint256 tokenId) public view returns (bool) {
    // Ensure the token exists
    require(
      nftContract.ownerOf(tokenId) != address(0),
      'ERC721: operator query for nonexistent token'
    );
    uint256 tokenAgeInSeconds = block.timestamp -
      nftContract.getTokenLastTransferredTimestamp(tokenId);
    // Assuming tokenAgeInSeconds is the age of the token since it's holding the nft, check if it's over minimum days held
    return tokenAgeInSeconds >= minimumDaysHeld;
  }
```

The above function only returns `true` if the `tokenAgeInSeconds` which is basically the `lastTokenTransferredTimestamp` variable is >= `minimumDaysHeld` which in this case was set to 3 days.

The `lastTokenTransferredTimestamp` is defined as the last time in which a transfer of a particular tokenId was being made and this is being monitored by an ERC721 hook. This means the protocol intends to allow for a cool down period of at least 3 days for every token that has recently been transfered to other users before they can be nuked.

**ERC721 Hook implementation**

```solidity
    function _beforeTokenTransfer(
    address from,
    address to,
    uint256 firstTokenId,
    uint256 batchSize
  ) internal virtual override {
    super._beforeTokenTransfer(from, to, firstTokenId, batchSize);

    uint listedId = entityForgingContract.getListedTokenIds(firstTokenId);
    /// @dev don't update the transferred timestamp if from and to address are same
    // @audit
    if (from != to) {
      lastTokenTransferredTimestamp[firstTokenId] = block.timestamp;
    }

    if (listedId > 0) {
      IEntityForging.Listing memory listing = entityForgingContract.getListings(
        listedId
      );
      if (
        listing.tokenId == firstTokenId &&
        listing.account == from &&
        listing.isListed
      ) {
        entityForgingContract.cancelListingForForging(firstTokenId);
      }
    }

    require(!paused(), 'ERC721Pausable: token transfer while paused');
  }
```

From the above hook implementation, the protocol only updat
wrong contracts deployment, can lead to severe results where tokens has no entropy.
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/test/TraitForgeNft.test.ts#L51


# Vulnerability details

## Impact
Considering the deployment of the real protocol will be deployed the same way as the testnet game was deployed (as said in the discord), if the project will succeed, and the minting pace will be very fast (for example, as we have seen in the NFT bull craze) some tokens will be with 0 entropy.

## Proof of Concept
in the test net deployment, and as seen in the tests, the contracts are being deployed with everything set and only `EntropyGenerator::writeEntropyBatch1` is being called.
which means, that at first, only the tokens related to the Batch1 will have entropy set.

TraitForgeNft functions (please go to the last page, and view the orders):
https://sepolia.etherscan.io/txs?a=0x9ee29226bc3a3c2da4f95b8722f55ac467eb1ded&p

after [setRootHash](https://sepolia.etherscan.io/tx/0x5fcc5dc0f758ddfd9bc42dbe7051a8e4cceced1f3a6d9c9cd0acc92cb402b1b9) function is being called, together with: [writeEntropyBatch1](https://sepolia.etherscan.io/tx/0xee80e2f94b377507fc38072b80f5e5f123c0085c8209b69cb4175228f4f5a4f5)

people can start and mint.
there was a 12 seconds delay between writeEntropyBatch1 function and [writeEntropyBatch2](https://sepolia.etherscan.io/tx/0x753183939fbeecc013065d010aa6e8384dec2b8c2ba3aeb56ce8794ac3178bea) function calls.

which means, that if at this delay period, a massive amount of mints would have been occur, tokens that their entropy is being defined by the batch2 or batch3 functions, will be with entropy = 0.

### Proof of code
install foundry and run this test:

```
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {Test, console} from "../lib/forge-std/src/Test.sol";
import {StdInvariant} from "../lib/forge-std/src/StdInvariant.sol";
import {TraitForgeNft} from "../contracts/TraitForgeNft/TraitForgeNft.sol";
import {EntropyGenerator} from "../contracts/EntropyGenerator/EntropyGenerator.sol";
import {EntityForging} from "../contracts/EntityForging/EntityForging.sol";
import {Airdrop} from "../contracts/Airdrop/Airdrop.sol";
import {NukeFund} from "../contracts/NukeFund/NukeFund.sol";
import {DevFund} from "../contracts/DevFund/DevFund.sol";

contract stateLessTest is Test {
    TraitForgeNft nft;
    EntropyGenerator eg;
    EntityForging ef;
    Airdrop airdrop;
    NukeFund nf;
    DevFund df;
    uint8 numOfGodOnes;
    uint256 numOfTrials;

    struct DevInfo {
        uint256 weight;
        uint256 rewardDebt;
        uint256 pendingRewards;
    }

    function setUp() public {
        nft = new TraitForgeNft();
        eg = new EntropyGenerator(address(nft));
        ef = new EntityForging(address(nft));
        vm.prank(address(nft));
        airdrop = new Airdrop();
        address test = makeAddr("test");
        nf = new NukeFund(address(nft), address(airdrop), payable(test), payable(test));
        nft.setEntityForgingContract(add
Nuking is broken for approved personnels
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L182


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L153-L182

```solidity
  function nuke(uint256 tokenId) public whenNotPaused nonReentrant {
    require(
      nftContract.isApprovedOrOwner(msg.sender, tokenId),
      'ERC721: caller is not token owner or approved'
    );
    require(
      nftContract.getApproved(tokenId) == address(this) ||
        nftContract.isApprovedForAll(msg.sender, address(this)),
      'Contract must be approved to transfer the NFT.'
    );
    require(canTokenBeNuked(tokenId), 'Token is not mature yet');

    uint256 finalNukeFactor = calculateNukeFactor(tokenId); // finalNukeFactor has 5 digits
    uint256 potentialClaimAmount = (fund * finalNukeFactor) / MAX_DENOMINATOR; // Calculate the potential claim amount based on the finalNukeFactor
    uint256 maxAllowedClaimAmount = fund / maxAllowedClaimDivisor; // Define a maximum allowed claim amount as 50% of the current fund size

    // Directly assign the value to claimAmount based on the condition, removing the redeclaration
    uint256 claimAmount = finalNukeFactor > nukeFactorMaxParam
      ? maxAllowedClaimAmount
      : potentialClaimAmount;

    fund -= claimAmount; // Deduct the claim amount from the fund

    nftContract.burn(tokenId); // Burn the token
    (bool success, ) = payable(msg.sender).call{ value: claimAmount }('');
    require(success, 'Failed to send Ether');

    emit Nuked(msg.sender, tokenId, claimAmount); // Emit the event with the actual claim amount
    emit FundBalanceUpdated(fund); // Update the fund balance
  }
```

This is the function that's used for nuking, now from the snippet and [documentation](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L156) we can see that an approved owner is expected to be able to nuke in place of who have approved them, this logic _(i.e not nuking, but approving other contracts to take care of txs is quite common in the NFT space)_.

Problem here is that even when the owner is not the one who is trying to nuke by themselves (i.e they are not `msg.sender`), the function still queries `isApprovedForAll` on them, CC https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L158-L162

```solidity
    require(
      nftContract.getApproved(tokenId) == address(this) ||
        nftContract.isApprovedForAll(msg.sender, address(this)),
      'Contract must be approved to transfer the NFT.'
    );
```

However this would fail cause the `msg.sender` is not the nft owner and as such `isApprovedForAll(msg.sender, address(this)` would be false and the query would revert.

### Coded POC
Upgraded Q -> 2 from #1076 [1725567784981]
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

Judge has assessed an item in Issue #1076 as 2 risk. The relevant finding follows:

 [L-3] The Golden God NFT can appear twice in one generation
The Golden God entity in the TraitForge protocol is a unique entity that has the highest entropy of 999999 and is the most sought-after of all entities. Based on the protocol documentation this entity should only appear once per generation, however there is a small chance for it to appear twice:

Imagine the following scenario:

slotIndexSelectionPoint = 600, numberIndexSelectionPoint = 5, and entropySlots[700] = 123456999999123456789012345678901234567890123456789012345678901234567890
getNextEntropy() is called.
Call 1: currentSlotIndex = 0, currentNumberIndex = 0

getEntropy(0, 0):
  position = 0 * 6 = 0
  slotValue = entropySlots[0] (let's say it's 987654321987654321...)
  entropy = (987654321987654321... / 10^72) % 1000000 = 987654
  paddedEntropy = 987654 * (10^(6 - 6)) = 987654
Return: 987654
…(many calls later)…

Call 7806: currentSlotIndex = 600, currentNumberIndex = 5

getEntropy(600, 5):
  if (600 == slotIndexSelectionPoint && 5 == numberIndexSelectionPoint):
    return 999999
Return: 999999
Call 9101: currentSlotIndex = 700, currentNumberIndex = 6

getEntropy(700, 6):
  position = 6 * 6 = 36
  slotValue = 123456999999123456789012345678901234567890123456789012345678901234567890
  entropy = (123456999999123456789012345678901234567890123456789012345678901234567890 / 10^(72-36)) % 1000000
          = (123456999999123456... / 10^36) % 1000000
          = 999999
  paddedEntropy = 999999 * (10^(6 - 6)) = 999999
Return: 999999
From the above, it can be seen that in a very small percentage of entropies, the Golden God can be produced twice. The chance is around 1%. This is a tough issue to handle, but maybe an easy solution will be to have a kind of storage to keep track of how many Golden Gods have been produced per generation.