The price increase in TraitForgeNft::calculateMintPrice has two parts: priceIncrement, which increases the price per mint, and priceIncrementByGen, which is meant to increase price per generation. However, the current calculation is incorrect, as the per-generation increase is not flat and is also dependent on currentGenMintCount.
Note that: The current implementation of the TraitForgeNft::calculateMintPrice function suggests that the priceIncrementByGen is incorrectly influenced by the currentGenMintCount. This behavior is unexpected because the generation increment should be flat and independent of the mint count within the current generation. Given the nature of the described issue—where the price increment calculation appears to be logically flawed—it is more likely that this is a bug rather than a deliberate design choice. The expected behavior (a flat increment per generation) is logical from an economic perspective, ensuring that each new generation of tokens has a predictable and consistent price increase. The current behavior (where the generation increment depends on the mint count) seems like it could easily result from a misunderstanding or error in implementing the intended functionality. This is further supported by the significant discrepancy between the actual and expected mint prices observed in the proof of concept.
Impact
The current implementation leads to incorrect mint price calculations, where the price does not increase as intended with each new generation. This results in significantly lower prices than expected for new token mints, causing the project to earn less revenue than planned.
Over time, this could have substantial financial implications, particularly if the project relies on minting revenue for sustainability and growth. Additionally, users may perceive the pricing model as flawed or inconsistent, potentially impacting user trust and engagement with the project.
Correcting this issue is crucial to ensure the economic model functions as designed, maintaining fair pricing for users and adequate revenue for the project's ongoing development and operations.
Proof of Concept
The proof can be demonstrated using hardhat-foundry library and this foundry test suite:
Steps:
Mint initial 10000 tokens to reach the 1st generation threshold.
Mint first token in gen 2 and calculate the expected price with correct formula.
Assert the expected Price > actual Price.
The results will be as such for first token in gen 2:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import '../../contracts/Airdrop/Airdrop.sol';
import '../../contracts/DAOFund/DAOFund.sol';
import '../../contracts/DevFund/DevFund.sol';
import '../../contracts/EntityForging/EntityForging.sol';
import '../../contracts/EntityTrading/EntityTrading.sol';
import '../../contracts/EntropyGenerator/EntropyGenerator.sol';
import '../../contracts/NukeFund/NukeFund.sol';
import '../../contracts/TraitForgeNft/TraitForgeNft.sol';
import '../../contracts/Trait/Trait.sol';
import '../../contracts/DAOFund/IUniswapV2Router.sol';
import { Test, console2 } from 'forge-std/Test.sol';
contract TFGNFTTest is Test {
Airdrop public airdrop;
DAOFund public dao;
DevFund public dev;
EntityForging public forge;
EntityTrading public trade;
EntropyGenerator public entropy;
NukeFund public nuke;
TraitForgeNft public nft;
Trait public traitToken;
IUniswapV2Router01 router;
address userA = makeAddr('userA');
address userB = makeAddr('userB');
address public user1 = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
address public user2 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
address public owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
function setUp() public {
vm.roll(1); //111 for fetchListingDOS
vm.startPrank(owner);
traitToken = new Trait('Trait', 'TR', uint8(18), 1000000e18);
airdrop = new Airdrop();
router = IUniswapV2Router01(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
dao = new DAOFund(address(traitToken), address(router));
dev = new DevFund();
nft = new TraitForgeNft();
forge = new EntityForging(address(nft));
trade = new EntityTrading(address(nft));
entropy = new EntropyGenerator(address(nft));
nuke = new NukeFund(
address(nft),
address(airdrop),
payable(address(dev)),
payable(address(dao))
);
/// dao init ///
/// dev init ///
/// nft inits ///
nft.setAirdropContract(address(airdrop));
airdrop.transferOwnership(address(nft));
entropy.writeEntropyBatch1();
entropy.writeEntropyBatch2();
entropy.writeEntropyBatch3();
entropy.transferOwnership(address(nft));
assertEq(entropy.getLastInitializedIndex(), 770);
nft.setEntropyGenerator(address(entropy));
nft.setEntityForgingContract(address(forge));
nft.setNukeFundContract(payable(address(nuke)));
trade.setNukeFundAddress(payable(address(nuke)));
nft.setRootHash(
0x55e8063f883b9381398d8fef6fbae371817e8e4808a33a4145b8e3cdd65e3926
);
vm.stopPrank();
}
function testMintPriceIncrementForGenIsWrong2() public {
vm.warp(block.timestamp + 24 hours + 1);
// mint intial tokens to reach the 1st generation treshold
this.mintTokens(10000);
uint256 balanceBefore = address(this).balance;
// mint last token in gen 1 to see if the formula is correct.
this.mintTokens(1);
uint256 mintPrice = balanceBefore - address(this).balance;
console2.log(mintPrice); //250 000 000 000 000 000
// mint first token in gen 2 and calculate the expected price with correct formula
uint256 expectedPrice = nft.startPrice() +
0.0000245 ether *
nft.generationMintCounts(nft.currentGeneration()) +
(nft.currentGeneration() - 1) *
nft.priceIncrementByGen() *
10000; //55 024 500 000 000 000
balanceBefore = address(this).balance;
this.mintTokens(1);
mintPrice = balanceBefore - address(this).balance; //5 029 500 000 000 000
// expectedPrice (correct formula)
assert(expectedPrice > mintPrice);
}
///helper functions
function mintTokens(uint256 iterations) external {
bytes32[] memory proof;
for (uint256 i = 0; i < iterations; i++) {
nft.mintToken{ value: 1000000 ether }(proof); //just a big value so it never reverts.
}
}
}
Tools Used
Manual Review, hardhat-foundry
Recommended Mitigation Steps
To ensure the mint price increases correctly by generation, the formula should be adjusted to apply a flat increment per generation, independent of the current generation mint count. This can be achieved by modifying the calculateMintPrice function to incorporate the generation increment correctly.
Lines of code
https://github.com/code-423n4/2024-07-traitforge/blob/279b2887e3d38bc219a05d332cbcb0655b2dc644/contracts/TraitForgeNft/TraitForgeNft.sol#L227
Vulnerability details
The price increase in
TraitForgeNft::calculateMintPrice
has two parts:priceIncrement
, which increases the price per mint, andpriceIncrementByGen
, which is meant to increase price per generation. However, the current calculation is incorrect, as the per-generation increase is not flat and is also dependent oncurrentGenMintCount
.the
priceIncrement
is increases each time the generation is incremented using_incrementGeneration
As such, the existing formula for mint price is:
The correct formula should ensure that the generation increment is flat and does not change according to currentGenerationMintCount:
Note that: The current implementation of the
TraitForgeNft::calculateMintPrice
function suggests that thepriceIncrementByGen
is incorrectly influenced by thecurrentGenMintCount
. This behavior is unexpected because the generation increment should be flat and independent of the mint count within the current generation. Given the nature of the described issue—where the price increment calculation appears to be logically flawed—it is more likely that this is a bug rather than a deliberate design choice. The expected behavior (a flat increment per generation) is logical from an economic perspective, ensuring that each new generation of tokens has a predictable and consistent price increase. The current behavior (where the generation increment depends on the mint count) seems like it could easily result from a misunderstanding or error in implementing the intended functionality. This is further supported by the significant discrepancy between the actual and expected mint prices observed in the proof of concept.Impact
The current implementation leads to incorrect mint price calculations, where the price does not increase as intended with each new generation. This results in significantly lower prices than expected for new token mints, causing the project to earn less revenue than planned. Over time, this could have substantial financial implications, particularly if the project relies on minting revenue for sustainability and growth. Additionally, users may perceive the pricing model as flawed or inconsistent, potentially impacting user trust and engagement with the project. Correcting this issue is crucial to ensure the economic model functions as designed, maintaining fair pricing for users and adequate revenue for the project's ongoing development and operations.
Proof of Concept
The proof can be demonstrated using hardhat-foundry library and this foundry test suite: Steps:
The results will be as such for first token in gen 2:
Tools Used
Manual Review, hardhat-foundry
Recommended Mitigation Steps
To ensure the mint price increases correctly by generation, the formula should be adjusted to apply a flat increment per generation, independent of the current generation mint count. This can be achieved by modifying the
calculateMintPrice
function to incorporate the generation increment correctly.Assessed type
Other