xanoutas/solidity-security-findings huggingface dataset:
Improper handling of final generation minting
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L345-L355


# Vulnerability details

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L345-L355

## Summary

`TraitForgeNft` does not explicitly address what should happen when the maximum number of generations (`maxGeneration`) is reached. As a result, minting operations could continue beyond the intended limit, leading to an overflow of token creation or unintended behavior.


## Impact

Unintended minting beyond `maxGeneration`. Without a proper check, the contract could allow the minting of tokens even after reaching the `maxGeneration` limit. This could lead to an excess supply of NFTs, diluting the intended scarcity and affecting the value of existing tokens. By not enforcing a generation cap, the contract fails to maintain control over the minting lifecycle. 

If the logic for incrementing generations does not prevent unauthorized actions once the final generation is reached, it could open up for malicious minting activities.

## Proof of Concept

The `mintToken` function lacks a check if the `maxGeneration` is reached and so `_mintInternal` is called on line 193:

```javascript
contracts/TraitForgeNft/TraitForgeNft.sol

181:   function mintToken(
182:     bytes32[] calldata proof
183:   )
184:     public
185:     payable
186:     whenNotPaused
187:     nonReentrant
188:     onlyWhitelisted(proof, keccak256(abi.encodePacked(msg.sender)))
189:   {
190:     uint256 mintPrice = calculateMintPrice();
191:     require(msg.value >= mintPrice, 'Insufficient ETH send for minting.');
193:->   _mintInternal(msg.sender, mintPrice);
195:     uint256 excessPayment = msg.value - mintPrice;
196:     if (excessPayment > 0) {
197:       (bool refundSuccess, ) = msg.sender.call{ value: excessPayment }('');
198:       require(refundSuccess, 'Refund of excess payment failed.');
199:     }
200:   }
```

`_mintInternal` function only checks the current generation minted tokens count and then `_incrementGeneration` is called on line 282:
```javascript
contracts/TraitForgeNft/TraitForgeNft.sol

280:   function _mintInternal(address to, uint256 mintPrice) internal {
281:     if (generationMintCounts[currentGeneration] >= maxTokensPerGen) {
282:->     _incrementGeneration();
283:     }
        . . .
309:   }

```

Here we can see the `_incrementGeneration` function, where the improper handling of the final generation occurs:

```javascript
contracts/TraitForgeNft/TraitForgeNft.sol

345:   function _incrementGeneration() private {
346:     require(
347:       generationMintCounts[currentGeneration] >= maxTokensPerGen,
348:       'Generation limit not yet reached'
349:     );
350:     currentGeneration++;
351:     generationMintCounts[currentGeneration] = 0;
352:     priceIncrement = priceIncrement + priceIncrementByGen;
353:     entropyGenerator.initializeAlphaIndices();
354:     emit GenerationIncr
Users who buy NFT are excluded from airdrop
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L63-L92
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L296-L298


# Vulnerability details

## Impact
Users who buy NFT are excluded from airdrop

## Proof of Concept
Here's the documentation:

```
TraitForge will receive an airdrop of $TRAIT tokens, which can be initiated at any time. Each instance of direct minting or forging to mint is recorded in the Airdrop Contract, along with the entropy of the minted entity. This process establishes a comprehensive tracking system for the total number of entities minted by each player and their corresponding entropy. In the event that a player 'nukes' an entity, the entropy allocated by that entity is removed. The cumulative entropy accrued by a player determines their final share of the airdrop. It is anticipated that the airdrop will be initiated within the first year of gameplay to facilitate a broader distribution of $TRAIT tokens.
```
https://github.com/TraitForge/GitBook/blob/main/GamePlay/%24TRAIT%20Token%20Airdrop.md

Based on the above, it is only users who minted directly or forge to mint that are eligible to airdrop. And that a user can be deprived of an airdrop only when a user nukes his token.

Now, the issue is a user who calls the buyNFT() and bought NFT is not recoreded in the airdrop contract or entitled to airdrop.

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L63-L92

Note that a user can mint and immediately list the minted token for sale. In this instance, the token is not nuked but rather transferred. And the token entropy is sitll in existence and calculated right from the time the token was minted.

The new owner of the token ought to be eligible for airdrop (if airdrop has not been done). But this is not the case.

## Tools Used
Manual review

## Recommended Mitigation Steps
The airdrop eligibility right of the previous token owner should be transferred to the new owner upon calling buyNFT().




## Assessed type

Context
Generations can be incremented beyond the maximum allowed
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L345-L355


# Vulnerability details

## Impact
Each generation has a predefined cap on the number of tokens that can be minted. Once this cap is reached, the [_incrementGeneration](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L345-L355) function is triggered to advance the game to the next generation. The issue is that **the function does not check if the new generation exceeds the maximum generation allowed**:
```solidity
function _incrementGeneration() private {
    require(
      generationMintCounts[currentGeneration] >= maxTokensPerGen,
      'Generation limit not yet reached'
    );
    currentGeneration++;
    generationMintCounts[currentGeneration] = 0; /
    priceIncrement = priceIncrement + priceIncrementByGen;
    entropyGenerator.initializeAlphaIndices();

    // @audit No check if the new generation exceeds the maximum generation allowed
    emit GenerationIncremented(currentGeneration);
}
```
As shown above, the generation is incremented without checking if it exceeds the maximum generation allowed. This allows minting tokens on generations beyond `maxGeneration`:
```solidity
function _mintInternal(address to, uint256 mintPrice) internal {
    if (generationMintCounts[currentGeneration] >= maxTokensPerGen) {
      _incrementGeneration();
    }

    _tokenIds++;
    uint256 newItemId = _tokenIds;
    _mint(to, newItemId);
    uint256 entropyValue = entropyGenerator.getNextEntropy();

    tokenCreationTimestamps[newItemId] = block.timestamp;
    tokenEntropy[newItemId] = entropyValue;
    tokenGenerations[newItemId] = currentGeneration;
    generationMintCounts[currentGeneration]++;
    initialOwners[newItemId] = to;

    if (!airdropContract.airdropStarted()) {
      airdropContract.addUserAmount(to, entropyValue);
    }

    emit Minted(
      msg.sender,
      newItemId,
      currentGeneration,
      entropyValue,
      mintPrice
    );

    _distributeFunds(mintPrice);
  }
```

## Proof of Concept
Copy/Paste the following test in `test/TraitForgeNft.test.ts`
```ts
it('No check for max generation', async () => {
  await nft.setMaxGeneration(1);
  await nft.setMaxTokensPerGen(3); // We added this function in the smart contract for test simplicity
  await entropyGenerator.transferOwnership(await nft.getAddress()) // we need to transfer ownership so _incrementGeneration does not revert

  // mint token 1 to user 1 => gen(token1) = 1
  await nft.connect(user1).mintToken(merkleInfo.whitelist[1].proof, {
    value: ethers.parseEther('1'),
  });

  // mint token 2 to user 1 => gen(token2) = 1
  await nft.connect(user1).mintToken(merkleInfo.whitelist[1].proof, {
    value: ethers.parseEther('1'),
  });

  // mint token 3 to user 2 => gen(token3) = 1 
  await nft.connect(user2).mintToken(merkleInfo
`_incrementGeneration` resets `generationMintCounts` and ignores the tokens already forged on the new generation
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L351


# Vulnerability details

## Impact
Each generation has a predefined cap on the number of tokens that can be minted. Once this cap is reached, the [_incrementGeneration](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L345-L355) function is triggered to advance the game to the next generation. The issue is that the function sets the `generationMintCounts` of the new generation to zero, **ignoring the tokens that were already forged into the new generation**:
```solidity
function _incrementGeneration() private {
    require(
      generationMintCounts[currentGeneration] >= maxTokensPerGen,
      'Generation limit not yet reached'
    );
    currentGeneration++;
    generationMintCounts[currentGeneration] = 0; // @audit Wrong update
    priceIncrement = priceIncrement + priceIncrementByGen;
    entropyGenerator.initializeAlphaIndices();
    emit GenerationIncremented(currentGeneration);
}
```
As shown above, the mint count for the new generation is *reset* to zero `generationMintCounts[currentGeneration] = 0;`. When forging two tokens, the new token will always be of a [new generation](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L163):
```solidity
function forge(
    address newOwner,
    uint256 parent1Id,
    uint256 parent2Id,
    string memory
  ) external whenNotPaused nonReentrant returns (uint256) {
    require(
      msg.sender == address(entityForgingContract),
      'unauthorized caller'
    );
    uint256 newGeneration = getTokenGeneration(parent1Id) + 1;

    /// Check new generation is not over maxGeneration
    require(newGeneration <= maxGeneration, "can't be over max generation");

    // ...
}
```
Therefore, if tokens were already forged into the new generation, the mint counts will be ignored when incrementing the current generation and will be set to zero, resulting in `generationMintCounts` containing an *invalid* state. This has two impacts:
- It allows for minting more tokens than intended for the generation, exceeding the generation cap
- `calculatePrice` will return unintended lower values because it depends on `generationMintCounts`
## Proof Of Concept
Consider the following example demonstrated by runnable PoC below:
- The maximum number of tokens per generation is set to `3`.
- `User1` mints `2` tokens and `User2` mints `1` token. All three tokens belong to generation `1`.
- `User1` lists his token 1 for forging and `User2` forges the listed token. 
    - The new token, with ID 4, belongs to generation `2` since forging new tokens always results in a [new generation](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/Tr
Wrong calculation of `taxCut` can lead to incorrect distribution of funds
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityForging/EntityForging.sol#L146
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L72
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L41


# Vulnerability details

## Impact
`taxCut` is a non-constant variable, being set to `10%` on deploy. It has a `setTaxCut()` function which allows the `taxCut` value to be changed at any time by the contract owner. The `taxCut` amount is used on three places through the protocol:
1) In `NukeFund::receive()` upon receiving ETH the `taxCut` is calculated and send to the Dev/DAO Fund.
2) In `EntityTrading::buyNFT()` upon buying a NFT the `nukeFundContribution` variable represents the `taxCut` end value that is being transfered to the `nukeFund` address.
3) In `EntityForging::forgeWithListed()` upon forging a NFT the `devFee` variable represents the `taxCut` end value that is being transfered to the `nukeFund` address.

The `taxCut` calculation may work with the current value set on deploying of the contracts which is `10%` but if changed in the future by the contract owner it will return wrong values making the protocol/seller receiving less or more fees depending of the `taxCut` value being set.

[NukeFund.sol::receive()](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/NukeFund/NukeFund.sol#L41)
```solidity
  receive() external payable {
  @>>>  uint256 devShare = msg.value / taxCut; // Calculate developer's share (10%)
  @>>>  uint256 remainingFund = msg.value - devShare; // Calculate remaining funds to add to the fund


    fund += remainingFund; // Update the fund balance


    if (!airdropContract.airdropStarted()) {
      (bool success, ) = devAddress.call{ value: devShare }('');
      require(success, 'ETH send failed');
      emit DevShareDistributed(devShare);
    } else if (!airdropContract.daoFundAllowed()) {
      (bool success, ) = payable(owner()).call{ value: devShare }('');
      require(success, 'ETH send failed');
    } else {
      (bool success, ) = daoAddress.call{ value: devShare }('');
      require(success, 'ETH send failed');
      emit DevShareDistributed(devShare);
    }


    emit FundReceived(msg.sender, msg.value); // Log the received funds
    emit FundBalanceUpdated(fund); // Update the fund balance
  }
```

[EntityTrading.sol::buyNFT()](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityTrading/EntityTrading.sol#L72)
```solidity
  function buyNFT(uint256 tokenId) external payable whenNotPaused nonReentrant {
    ......
    ......
    ......

    //transfer eth to seller (distribute to nukefund)
@>>>    uint256 nukeFundContribution = msg.value / taxCut;
@>>>    uint256 sellerPro
Flawed token limit check in `mintWithBudget` allows unlimited minting per generation
Audit: 2024-07-traitforge-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L202-L226


# Vulnerability details

## Impact
The `mintWithBudget` function uses an incorrect condition to check if the token limit for the current generation has been reached. Users could mint more tokens than intended for a given generation.

## Proof of Concept
Navigating to [`mintWithBudget` function:](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L202-L226)

```solidity
function mintWithBudget(
  bytes32[] calldata proof
)
  public
  payable
  whenNotPaused
  nonReentrant
  onlyWhitelisted(proof, keccak256(abi.encodePacked(msg.sender)))
{
  uint256 mintPrice = calculateMintPrice();
  uint256 amountMinted = 0;
  uint256 budgetLeft = msg.value;

  while (budgetLeft >= mintPrice && _tokenIds < maxTokensPerGen) {
    _mintInternal(msg.sender, mintPrice);
    amountMinted++;
    budgetLeft -= mintPrice;
    mintPrice = calculateMintPrice();
  }
  if (budgetLeft > 0) {
      (bool refundSuccess, ) = msg.sender.call{ value: budgetLeft }('');
      require(refundSuccess, 'Refund failed.');
    }
}
```

The condition `_tokenIds < maxTokensPerGen` in the while loop is incorrect because:

`_tokenIds` is a global counter for all tokens across all generations and `maxTokensPerGen` is the limit for each individual generation.

This condition allows minting to continue as long as the total number of tokens minted across all generations is less than `maxTokensPerGen`, instead of checking the limit for the current generation.

## Tools used
Manual review

## Recommended Mitigation Steps
Replace the incorrect condition with a check against the current generation's mint count:

```diff
function mintWithBudget(
  bytes32[] calldata proof
)
  public
  payable
  whenNotPaused
  nonReentrant
  onlyWhitelisted(proof, keccak256(abi.encodePacked(msg.sender)))
{
  uint256 mintPrice = calculateMintPrice();
  uint256 amountMinted = 0;
  uint256 budgetLeft = msg.value;

- while (budgetLeft >= mintPrice && _tokenIds < maxTokensPerGen) {
+ while (budgetLeft >= mintPrice && generationMintCounts[currentGeneration] < maxTokensPerGen) {
    _mintInternal(msg.sender, mintPrice);
    amountMinted++;
    budgetLeft -= mintPrice;
    mintPrice = calculateMintPrice();
  }
 if (budgetLeft > 0) {
      (bool refundSuccess, ) = msg.sender.call{ value: budgetLeft }('');
      require(refundSuccess, 'Refund failed.');
    }
}
```
Also consider adding this `+ require(currentGeneration <= maxGeneration, "Max generation reached");` check to ensure that `currentGeneration` hasn't exceeded `maxGeneration` before allowing any mints


## Assessed type

Other
A chain re-org would make a forger forge the wrong NFT and could even lose funds due to this
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityForging/EntityForging.sol#L102


# Vulnerability details


## Proof of Concept

Protocol is to deploy on Base network

Protocol allows forging via [Forging.sol#L102](https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntityForging/EntityForging.sol#L102)

When forging there is a need to get the listed NFT's by their Ids.

This then means that this scenario is possible.

- Two different integrators mint & list their NFTs for forging
- NFT A is very rare, as such the owner sets the forge fee to be `1.5 `Ether.
- NFT B is not too rare so the fee is `0.1` Ether.
- They both list their NFTs for forging.
- The forger seeing this attempts to forge NFT A so as to own it.
- A chain reorg happens and the order of transactions is swapped.
- The forger ends up forging the wrong NFT _less rare_, and they end up losing `1.4 ETH` in the process.

## Impact

A chain re-org would make a forger forge the wrong NFT and could even lose funds due to this, since they pay too much for forging and end up forging the wrong NFT.

## Recommended Mitigation Steps

When forging the listed tokens consider the forger to indicate the fees of the NFTs they would like to forge, so in our case here even if the re-org happens the tx would revert since the fees wouldn't match.



## Assessed type

Timing
All entities will have 0 entropy
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntropyGenerator/EntropyGenerator.sol#L101-L120


# Vulnerability details

## Impact
All users minting a token will get entity with entropy as 0.

## Proof of Concept
The getNextEntropy() function can be called without the entropies being written into the entropySlots. Since there is no validation in the function to make sure that the writeEntropyBatch functions have been called beforehand. This would make any user who uses the mintToken() or mintWithBudget() function to receive entities with 0 as the entropy.
(https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/EntropyGenerator/EntropyGenerator.sol#L101-L120)

It's better to check that the initializeAlphaIndices() function has also been called before.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add the following validation to the getNextEntropy() function (at line 102):
`require(lastInitializedIndex == maxSlotIndex && slotIndexSelectionPoint != 0, "Entropy Slots not written yet");`


## Assessed type

Invalid Validation
Upgraded Q -> 2 from #1076 [1725656589063]
Audit: 2024-07-traitforge-findings
Severity: MEDIUM

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

 [L-1] Listing and delisting tokens in EntityTrading.sol extends nuking timeout
Whenever users mint or receive a new token they need to wait 3 days before being able to nuke. However, if they list their tokens for sale and choose to delist them, their nuking timeout will be reset and they will need to wait 3 more days before being able to nuke.

  function canTokenBeNuked(uint256 tokenId) public view returns (bool) {
__SNIP__
    // 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;
  }
function _beforeTokenTransfer(...) internal virtual override {
__SNIP__
    /// @dev don't update the transferred timestamp if from and to address are same
    if (from != to) {
@>      lastTokenTransferredTimestamp[firstTokenId] = block.timestamp; // Updates the token held timestamp even on marketplace listing and delisting
    }
__SNIP__
  }

This can be mitigated by checking the `to` and `from` and seeing if it matches the `EntityTrading` contract address.

# [L-2] Incorrect view functions present in the contract

Both `EntityForging.sol` and `EntropyGenerator.sol` contracts contain view functions that produce incorrect results:

```solidity
  function fetchListings() external view returns (Listing[] memory _listings) {
    _listings = new Listing[](listingCount + 1);
    for (uint256 i = 1; i <= listingCount; ++i) {
      _listings[i] = listings[i];
    }
  }
This view function will always add an additional empty listing at the beginning of the _listings array and at the end.

  function deriveTokenParameters(
    uint256 slotIndex,
    uint256 numberIndex
  )
    public
    view
    returns (
      uint256 nukeFactor,
      uint256 forgePotential,
      uint256 performanceFactor,
      bool isForger
    )
  {
    uint256 entropy = getEntropy(slotIndex, numberIndex);

    // example calcualtions using entropyto derive game-related parameters
@>    nukeFactor = entropy / 4000000;
    forgePotential = getFirstDigit(entropy);
    performanceFactor = entropy % 10;

    // exmaple logic to determine a boolean property based on entropy
    uint256 role = entropy % 3;
    isForger = role == 0;

    return (nukeFactor, forgePotential, performanceFactor, isForger); // return derived parammeters
  }
As per the documentation the forgePotential should be calculated as entropy / 40.