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

1 stars 0 forks source link

Discrepancy between nfts minted, price of nft when a generation changes & position of `_incrementGeneration()` inside `_mintInternal()` & `_mintNewEntity()` #564

Open howlbot-integration[bot] opened 2 months ago

howlbot-integration[bot] commented 2 months ago

Lines of code

https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L280-L283 https://github.com/code-423n4/2024-07-traitforge/blob/main/contracts/TraitForgeNft/TraitForgeNft.sol#L227-L232

Vulnerability details

The report covers two bugs:

Proof of Concept

1st Bug Scenario:

File: TraitForgeNft.sol

  function calculateMintPrice() public view returns (uint256) {
    uint256 currentGenMintCount = generationMintCounts[currentGeneration];
    uint256 priceIncrease = priceIncrement * currentGenMintCount;
    uint256 price = startPrice + priceIncrease;
    return price;
  }
File: TraitForgeNft.sol

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

Now comes the interesting part.

2nd Bug Scenario:

Now if the generationMintCounts is at 9999 & _mintNewEntity() is called internally through forging, a different scenario takes place.

File: TraitForgeNft.sol

  function _mintNewEntity(
    address newOwner,
    uint256 entropy,
    uint256 gen
  ) private returns (uint256) {
    require(
      generationMintCounts[gen] < maxTokensPerGen,
      'Exceeds maxTokensPerGen'
    );

>   _tokenIds++;
    uint256 newTokenId = _tokenIds;
    _mint(newOwner, newTokenId);

    tokenCreationTimestamps[newTokenId] = block.timestamp;
    tokenEntropy[newTokenId] = entropy;
    tokenGenerations[newTokenId] = gen;
>   generationMintCounts[gen]++;
    initialOwners[newTokenId] = newOwner;

    if (
>     generationMintCounts[gen] >= maxTokensPerGen && gen == currentGeneration
    ) {
      _incrementGeneration();
    }
    ...

Tools Used

Manual Review

Recommended Mitigation Steps

Fixing the bugs would require changes in the calculateMintPrice() & _mintInternal(). However the protocol will not be able to claim the the last calculated price as intended by the whitepaper but it would then align with the number of nfts minted & the price the user pays.

File: TraitForgeNft.sol

  function calculateMintPrice() public view returns (uint256) {
+  if (generationMintCounts[currentGeneration] > 0) {
      uint256 currentGenMintCount = generationMintCounts[currentGeneration];
      uint256 priceIncrease = priceIncrement * currentGenMintCount;
      uint256 price = startPrice + priceIncrease;
      return price;
   }
+  else {
+     uint256 price = startPrice + ((currentGeneration - 1) * priceIncrementByGen );
+     return price;
   }
  }
File: TraitForgeNft.sol

  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 (generationMintCounts[currentGeneration] >= maxTokensPerGen) {
+     _incrementGeneration();
+   }
    ...

Assessed type

Error

Shubh0412 commented 2 months ago

@koolexcrypto

The above report covers two different scenarios. The first issue is indeed covered in the issue #210 of which this is a duplicate.

The second issue is explained in the 2nd Bug Scenario: part of the report

Wrong positioning of _incrementGeneration() will lead to the user paying the starting price of 1st nft of generation 1 even when the nft being minted is the 1st nft of a different generation.

I clubbed both these issue since they had the same origin point (minting of first nft of a different generation).

A request to consider this report to be 'selected for report' for covering both possibilities of the vulnerability & detailed explanation.

c4-judge commented 1 month ago

koolexcrypto marked the issue as not a duplicate

c4-judge commented 1 month ago

koolexcrypto marked the issue as primary issue

c4-judge commented 1 month ago

koolexcrypto marked the issue as selected for report