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

0 stars 0 forks source link

EntityForging::forge() is too restrictive, resulting in users receiving less rewards than they should #91

Closed c4-bot-6 closed 1 month ago

c4-bot-6 commented 2 months ago

Lines of code

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

Vulnerability details

Impact

The TraitForge protocol allows users to forge two NFTs of the same generation in order to create a new NFT from the next generation by calling the EntityForging::forgeWithListed() function. For each NFT that is used for forging there are two mappings that tract the amount of times this NFT was used for forging mapping(uint256 => uint8) public forgingCounts; and how much time has passed since that NFT was used for forging for the first time mapping(uint256 => uint256) private lastForgeResetTimestamp; . The _resetForgingCountIfNeeded() function resets the mapping(uint256 => uint8) public forgingCounts;, if more than a year has passed since the NFT was used for forging for the first time. The EntityForging::forgeWithListed() function internaly calls the TraitForgeNft::forge() function:

  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");

    // Calculate the new entity's entropy
    (uint256 forgerEntropy, uint256 mergerEntropy) = getEntropiesForTokens(parent1Id, parent2Id);
    uint256 newEntropy = (forgerEntropy + mergerEntropy) / 2;

    // Mint the new entity
    uint256 newTokenId = _mintNewEntity(newOwner, newEntropy, newGeneration);

    return newTokenId;
  }

As can be seen from the above code snippet, the generation of the NFT a user will receive after forging is the generation of the NFTs supplied as params to the EntityForging::forgeWithListed() function (which have to be equal) + 1. Thus if both the parents are of generation 1, the forged NFT can only be of generation 2. Additionally there is a check that the new generation can't exceed the maxGeneration which is set to 10

require(newGeneration <= maxGeneration, "can't be over max generation");

An NFT can have two roles a forger and a merger, a forger can list his NFT via the listForForging() function and receive fees when someone merges with his NFT. Each NFT has a forgingPotential which is calculated in the following way:

uint8 forgePotential = uint8((entropy / 10) % 10);

The merger has to pay a fee to the forger in order to forge with his NFT and receive a new NFT. The ability of an NFT to be used for forging, is an important factor in determining the price of the NFT itself, as well as how much a user can profit by listing his NFT for forging, or merging with his NFT in order to receive a new NFT. There are several highly likely scenarios where users will receive much less rewards than they are supposed to.

Proof of Concept

Tools Used

Manual review

Recommended Mitigation Steps

Consider making the forging process less restrictive, for example modify the EntityForging::forgeWithListed() function to allow users to mint NFTs for each next generation, not only for the parent's next generation. Don't allow the _incrementGeneration() function to increase the currentGeneration to a number greater than the maxGeneration.

Assessed type

Context

c4-judge commented 1 month ago

koolexcrypto marked the issue as satisfactory

AtanasDimulski commented 1 month ago

Hey @koolexcrypto thanks for the swift judging! This issue has nothing to do with the vulnerability described in #211, this issue describes how users will actually forge fewer NFTs than they are supposed to, not more. This issue clearly describes why forging won't work as expected which breaks core protocol functionality and results in users forging fewer NFTs than they are supposed to, which is a direct loss of funds. This issue describes two broken core protocol invariants. Please have another look. Thank you for your time!

koolexcrypto commented 1 month ago

Could you please summarize the issue in one or two paragraphs at max?

Please include root cause, impact and the two broken core protocol invariants that are never clarified in your comment nor in the issue or even README page of the contest.

AtanasDimulski commented 1 month ago

Hey @koolexcrypto, The first invariant is that each NFT based on entropy has a forge potential - meaning the times it can be used for merging, although this is not specified as an invariant in the README, it is pretty logical that if an NFT has a forge potential of 3, it should be able to forge 3 times. The different scenarios of why this won't work as expected are described in the first part of the report. The main root cause is that NFTs that merge have to be of the same generation and can mint an NFT only of the next generation which should be capped at 10_000. The second functionality that is broken is that each year that passes the forging of the NFTs should be reset, so if an NFT with a forging potential of 3, already merged 3 times, and a year has passed since the last merging the NFT owner is supposed to be able to merge again. If the NFT is of generation 1 but after a year the current generation is 6, the NFT of generation 1 won't be able to forge. Again this is not a specified invariant in the README, however, it is a pretty logical one, why else implement that functionality? I believe the report is pretty clear and provides different highly possible scenarios. Hopefully, this comment cleared up what is meant by invariants in the report. Thank you for your time!

c4-judge commented 1 month ago

koolexcrypto marked the issue as not a duplicate

koolexcrypto commented 1 month ago

Thank you, just an advice . Please split the texts into paragraphs. Otherwise, it is not readable.

koolexcrypto commented 1 month ago

It is clear now that this is not a dup of #211 . However, I still don't see what is the issue here. having an NFT from generation 1 and that couldn't be forged with another token due to the diff in generation is clearly a game design.

it is pretty logical that if an NFT has a forge potential of 3, it should be able to forge 3 times

Inaccurate, 3 forge potential are 3 forge potential , it doesn't mean you can forge. This depends on many factors including the generation and the exhausted forge count ...etc. It is not an absolute and can not hold always, that's why naming it as invariant is incorrect.

The second functionality that is broken is that each year that passes the forging of the NFTs should be reset, so if an NFT with a forging potential of 3, already merged 3 times, and a year has passed since the last merging the NFT owner is supposed to be able to merge again. If the NFT is of generation 1 but after a year the current generation is 6, the NFT of generation 1 won't be able to forge

I understand your concern here. However it is a part of the game design.

Given this, the issue can still be a QA.

c4-judge commented 1 month ago

koolexcrypto removed the grade

c4-judge commented 1 month ago

koolexcrypto changed the severity to G (Gas Optimization)

c4-judge commented 1 month ago

koolexcrypto marked the issue as grade-c

AtanasDimulski commented 1 month ago

Hey @koolexcrypto, thanks for the feedback. With all the more than questionable submissions that you have accepted as valid meds and highs, stating that this is not a valid med is not serious. You accepted an issue where the sponsor clearly mentioned a variable won't be used in the code, and the argument you presented was that it is to keep everything fair, how exactly is this fair - the auditors who asked what the variable will be used for got penalized. Secondly, you made up arguments in issue #222 such as

From README

Core Mechanics
Entities: NFTs with unique traits and parameters impacting the game.

Same generation and same entropy goes against this Mechanic. Therefore, I still see it an issue.

Which doesn't state clearly that NFTs can't be with the same entropy, nevertheless you accepted is a valid med. Now in this issue where more than logical functionality of the protocol is broken, you are claiming that this is part of the game design. Inability to fully utilize the potential of your NFT is a game design? Can you please provide me a single sentence where that is stated, and that forging 3 times is only a possibility - it is an absolute, why else would you have forging possibility? For the second part of your argument, I don't know how you came up with that, - a functionality that doesn't work is part of the game design, this is not serious. I understand that judging NFT projects is a complex task and judging standards can vary from protocol to protocol but at least try to keep the same standards for the same contest. Thanks for your time.