Reproduced Exploit
TraitForge — the maximum number of generations is infinite
1. TraitForgeNft is meant to cap total supply at maxGeneration generations of maxTokensPerGen NFTs each (real protocol: 10 x 10,000 = 100,000). 2. Filling a generation's mint count calls _incrementGeneration, which re-checks the CURRENT generation's cap, then does currentGeneration++
Chain
Other
Category
logic
Date
Jul 2024
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena. Standalone Foundry PoC and full write-up: 37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/missing-bounds-check · vuln/access-control/insufficient-guard · vuln/tokenomics/supply-cap-breach
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC, noanvil_state. Full trace: output.txt. PoC: test/37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena_exp.sol.
Key info#
| Impact | HIGH — TraitForgeNft should cap total supply at maxGeneration x maxTokensPerGen NFTs, but _incrementGeneration never checks the new generation against maxGeneration, so the "cap" rolls over into a new, uncapped generation forever |
| Protocol | TraitForge — generative-trait NFT protocol |
| Vulnerable code | TraitForgeNft._incrementGeneration |
| Bug class | Missing bounds check: the generation counter is incremented with no upper-bound guard |
| Finding | Code4rena — TraitForge, 2024-07 · #37919 · [H-05] · reporter inzinko |
| Report | code4rena.com/reports/2024-07-traitforge |
| Source | AuditVault |
| Status | Audit finding — caught in review (not exploited on-chain), sponsor-confirmed. Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident. The real
TraitForgeNft sits inside a larger system (entropy generator, entity forging,
airdrop, whitelist minting); the PoC keeps the vulnerable
_incrementGeneration verbatim and reduces everything else to the minimal
mint/generation bookkeeping the bug needs.
TL;DR#
TraitForgeNftis meant to cap total supply atmaxGenerationgenerations ofmaxTokensPerGenNFTs each (real protocol: 10 x 10,000 = 100,000).- Filling a generation's mint count calls
_incrementGeneration, which re-checks the CURRENT generation's cap, then doescurrentGeneration++unconditionally — there is no check that the new value stays<= maxGeneration. - The instant the intended last generation fills up, the very next mint
silently opens generation N+1, which has its own fresh
maxTokensPerGenallowance and no memory of ever having "reached the max". - This repeats forever: every time a generation fills, a brand-new uncapped one opens. The finding's own PoC mints 110,000 NFTs against an advertised maximum of 100,000, twelve generations deep.
- Harm: the protocol's advertised fixed maximum supply is not enforced anywhere in the contract — anyone willing to pay the (rising) mint price can mint without limit, diluting the fixed-supply guarantee every holder and buyer relied on.
The vulnerable code#
TraitForgeNft.sol::_incrementGeneration (verbatim):
function _incrementGeneration() private {
require(
generationMintCounts[currentGeneration] >= maxTokensPerGen,
'Generation limit not yet reached'
);
currentGeneration++;
generationMintCounts[currentGeneration] = 0;
priceIncrement = priceIncrement + priceIncrementByGen;
entropyGenerator.initializeAlphaIndices();
emit GenerationIncremented(currentGeneration);
}
The recommended fix, per the finding:
function _incrementGeneration() private {
require(
generationMintCounts[currentGeneration] >= maxTokensPerGen,
'Generation limit not yet reached'
);
currentGeneration++;
+ require(currentGeneration <= maxGeneration,
+ 'Maximum generation reached'
+ );
generationMintCounts[currentGeneration] = 0;
...
}
Root cause#
_incrementGeneration validates the precondition for rolling over (the
current generation's mint count reached its cap) but never validates the
postcondition (the new generation must not exceed maxGeneration). The
two checks look similar but protect different things — the missing one is
exactly the check that would have made "maximum 10 generations" true.
Preconditions#
- None beyond ordinary usage: any user who mints enough tokens to fill the last intended generation automatically triggers the rollover into an unbounded new one. No special privilege or attacker setup required.
Attack walkthrough#
From output.txt, using a reduced scale (maxGeneration = 3,
maxTokensPerGen = 2, intended max supply = 6 — the real protocol uses
10 x 10,000 = 100,000; the bug and its mechanism are identical at any scale):
- Control — minting up to one short of the intended max supply keeps
currentGenerationwithin[1, maxGeneration], exactly as designed. - Minting the intended max supply's last token triggers
_incrementGenerationone time too many:currentGenerationbecomesmaxGeneration + 1(4) with no revert — the "maximum" has already been silently exceeded. - HARM: minting continues into this new, uncapped generation. An
additional
maxTokensPerGen(2) tokens are minted that should never have existed, bringing total supply to 8 against the advertised maximum of 6. - Nothing in the contract stops a further mint from repeating this indefinitely — the "10 generation" cap the protocol advertises is not a ceiling at all.
Diagrams#
Impact#
- The protocol's advertised, fixed maximum NFT supply
(
maxGeneration x maxTokensPerGen) is not actually enforced — anyone can keep minting past it as long as they pay the (increasing) mint price. - This dilutes the scarcity/rarity guarantees every buyer and holder relied on, undermines the tokenomics model built around a capped generational structure, and — if noticed — destroys trust in the protocol's stated supply limits.
- Because each new generation resets its own per-generation counter and price-increment schedule, the dilution is unbounded and repeats forever, not a one-time overshoot.
Remediation#
Add the missing postcondition check immediately after the increment, exactly as the finding recommends:
currentGeneration++;
require(currentGeneration <= maxGeneration, 'Maximum generation reached');
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: both tests PASS:
# test_mintingWithinCap_worksAsIntended (control: minting up to the cap behaves as designed)
# test_generationCap_isNotEnforced (harm: cap is breached with no revert, supply keeps growing)
PoC source: test/37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena_exp.sol
— the verbatim vulnerable _incrementGeneration, reduced to a small scale
(maxGeneration=3, maxTokensPerGen=2) so the full breach-and-overmint
sequence is cheap to run and replay; the missing bounds check is identical
at the real protocol's 10 x 10,000 scale.
Sources#
- AuditVault finding: 37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena.md
- Original report: code4rena.com/reports/2024-07-traitforge, finding #217
- Reduced source: quoted directly from the finding (
TraitForgeNft::_incrementGeneration), repo code-423n4/2024-07-traitforge
Taxonomy (AuditVault)#
genome:wrong-condition, ownership-transfer, access-rolessector:nft, tokenseverity:high
Reference: finding #37919 [H-05] by inzinko in the Code4rena TraitForge audit (Jul 2024) · curated by AuditVault
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena_exp (evm-hack-registry mirror).
- AuditVault finding: 37919-h-05-the-maximum-number-of-generations-is-infinite-code4rena.
Alerts & third-party analyses
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.