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

2 stars 1 forks source link

NFTs mature too slowly under default settings. #216

Open howlbot-integration[bot] opened 3 months ago

howlbot-integration[bot] commented 3 months ago

Lines of code

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

Vulnerability details

Bug description

The Nuke Fund accumulates ETH from new mints and economic activity. After a 3-day maturity period, anyone can nuke their entity to claim a share of the ETH in the Fund. Every entity has a parameter, called initialNukeFactor, set on mint which represents how much of the Fund can be claimed on nuke. The maximum total nukeFactor is 50%, expressed as 50_000.

The calculations of finalNukeFactor consist of adjustedAge, defaultNukeFactorIncrease and initialNukeFactor.

NukeFund.sol#L145-L148

uint256 initialNukeFactor = entropy / 40; // calcualte initalNukeFactor based on entropy, 5 digits
uint256 finalNukeFactor = ((adjustedAge * defaultNukeFactorIncrease) /
  MAX_DENOMINATOR) + initialNukeFactor;

The age is calculated via calculateAge() function.

NukeFund.sol#L121-L131

uint256 daysOld = (block.timestamp -
  nftContract.getTokenCreationTimestamp(tokenId)) /
  60 /
  60 /
  24;
uint256 perfomanceFactor = nftContract.getTokenEntropy(tokenId) % 10;
uint256 age = (daysOld *
  perfomanceFactor *
  MAX_DENOMINATOR *
  ageMultiplier) / 365; // add 5 digits for decimals

The idea behind age calculations is quite simple: we calculate how many days have passed since token creation and convert that into years.

However, the default value of the nukeFactorIncrease set to 250 is extremely low. While whitepaper mentions that the fastest maturing NFT should mature (reach nukeFactor of 50_000) in around 30 days and the slowest in 600 days, in reality it would take 4050 days for the best possible NFT to fully mature.

To showcase this let's do some math:

To mature NFT must reach finalNukeFactor of 50_000. The best possible NFT would have 25000 initialNukeFactor and performanceFactor of 9. defaultNukeFactor is set to 250.

finalNukeFactor = adjustedAge * defaultNukeFactorIncrease + initialNukeFactor

50000 = x * 250 + 25000

25000 = 250x

x = 100

adjustedAge = daysOld * performanceFactor / 365

100 = x * 9 / 365

9x = 100 * 365

x = 365000 / 9 = 4055.

For the best NFT to mature fully in 30 days, the defaultNukeFactor would need to be 135 times bigger, more precisely 33750.

Impact

NFTs mature extremely slowly with default settings, to the point where performanceFactor of an NFT does not play any role and finalNukeFactor is determined solely by the initialNukeFactor.

Proof of Concept

To set up the following POC in Foundry please follow the steps. Inside Hardhat project working directory:

Run it with forge test --match-test 'test_matureToSlowly' -vv.

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

import '../lib/forge-std/src/Test.sol';
import { console2 } from '../lib/forge-std/src/console2.sol';
import '../contracts/TraitForgeNft/TraitForgeNft.sol';
import '../contracts/EntropyGenerator/EntropyGenerator.sol';
import '../contracts/EntityForging/EntityForging.sol';
import '../contracts/Airdrop/Airdrop.sol';
import '../contracts/NukeFund/NukeFund.sol';

contract EntropyGeneratorTest is Test {
    TraitForgeNft public forgeNFT;
    EntropyGenerator public entropyGenerator;
    EntityForging public forgingContract;
    Airdrop public airdrop;
    NukeFund public nukeFund;
    address dev = makeAddr('dev');
    address dao = makeAddr('dao');

    function setUp() public {
        forgeNFT = new TraitForgeNft();
        entropyGenerator = new EntropyGenerator(address(forgeNFT));
        forgingContract = new EntityForging(address(forgeNFT));
        airdrop = new Airdrop();
        airdrop.transferOwnership(address(forgeNFT));
        nukeFund = new NukeFund(
            address(forgeNFT),
            address(airdrop),
            payable(dev),
            payable(dao)
        );
        nukeFund.setAgeMultplier(1);

        forgeNFT.setEntityForgingContract(address(forgingContract));
        forgeNFT.setEntropyGenerator(address(entropyGenerator));
        forgeNFT.setAirdropContract(address(airdrop));
    }

    function test_matureToSlowly() public {
        address user = makeAddr('user');
        vm.deal(user, 100000e18);
        bytes32[] memory proof;

        entropyGenerator.writeEntropyBatch1();
        entropyGenerator.writeEntropyBatch2();
        entropyGenerator.writeEntropyBatch3();

        uint256 initialNukeFactor;
        uint256 performanceFactor;
        uint256 tokenId = 0;

        vm.warp(1722790420);
        vm.warp(block.timestamp + 48 hours);

        while (initialNukeFactor != 24999 && performanceFactor != 9) {
            vm.prank(user);
            forgeNFT.mintToken{ value: 2e18 }(proof);
            tokenId++;
            initialNukeFactor = forgeNFT.getTokenEntropy(tokenId) / 40;
            performanceFactor = forgeNFT.getTokenEntropy(tokenId) % 10;
        }
        // Almost perfect NFT
        console2.log('initialNukeFactor:', initialNukeFactor);
        console2.log('performanceFactor:', performanceFactor);
        console2.log('--------------------------------------');

        // Judging by the docs this NFT should take around 30 days to mature fully
        vm.warp(block.timestamp + 30 days);

        // In 30 days NFT did not mature at all
        console2.log(
            'nukeFactor after 30 days: ',
            nukeFund.calculateNukeFactor(tokenId)
        );
        console2.log(
            'How much NFT matured:',
            nukeFund.calculateNukeFactor(tokenId) - initialNukeFactor
        );

        // It takes around 4000 days for an NFT to mature in perfect conditions
        vm.warp(block.timestamp + 4000 days);
        console2.log(
            'nukeFactor after 4000 days: ',
            nukeFund.calculateNukeFactor(tokenId)
        );
        console2.log(
            'How much NFT matured:',
            nukeFund.calculateNukeFactor(tokenId) - initialNukeFactor
        );
    }

The console output of the test:

Logs:
  initialNukeFactor: 24220
  performanceFactor: 9
  --------------------------------------
  nukeFactor after 30 days:  24404
  How much NFT matured: 184
  nukeFactor after 4000 days:  49062
  How much NFT matured: 24842

Recommended Mitigation

defaultNukeFactorIncrease variable should be set to reasonable value, that will correctly reflect the speed at which NFTs should mature. Judging by the docs, its value should be set to at least 33750.

Assessed type

Other

c4-judge commented 2 months ago

koolexcrypto marked the issue as satisfactory

c4-judge commented 2 months ago

koolexcrypto marked the issue as selected for report

c4-judge commented 2 months ago

koolexcrypto changed the severity to 3 (High Risk)

c4-judge commented 2 months ago

koolexcrypto changed the severity to 2 (Med Risk)