configurePass unconditionally sets passSupply[passId] = 0 at line 65. If the organizer calls this function after passes have already been sold (e.g., to update the price for a second wave), the supply counter loses all previously sold passes. The maxSupply check in buyPass then allows minting a full second batch, putting more passes into circulation than maxSupply intended.
The supply reset is unconditional. There is no check for passSupply[passId] > 0 and no way to update just the price without also resetting the supply. Even an honest organizer who simply wants to change the price of a pass tier will inadvertently reset the supply counter.
Consider the following scenario:
Organizer configures GENERAL pass: price = 1 ETH, maxSupply = 3
Three users buy passes. passSupply[GENERAL_PASS] = 3. Next buyer gets "Max supply reached".
Organizer wants to raise the price to 1.5 ETH for a "second wave" but keep the same maxSupply of 3
Organizer calls configurePass(1, 1.5 ether, 3). passSupply resets to 0.
Three more users buy passes at 1.5 ETH each. passSupply = 3 again.
Now 6 GENERAL passes exist in circulation, but maxSupply was 3.
This is not an admin-trust issue because the organizer is not acting maliciously. They are using configurePass for its intended purpose (updating price), and the supply reset is an unintended side effect with no way to avoid it.
Likelihood:
Reconfiguring a pass is a normal operational action (adjusting pricing based on demand, extending a sale). The function provides no way to update price without resetting supply, so any reconfiguration triggers the bug.
Impact:
Pass scarcity is the core value proposition of the tiered system. Backstage passes with a maxSupply of 50 could have 200 in circulation after a few reconfigurations. Existing holders suffer value dilution. The 3x BEAT multiplier for Backstage means each extra pass generates outsized BEAT rewards that drain memorabilia collections faster than intended.
The test configures GENERAL pass with maxSupply=3, sells 3 passes, then reconfigures to update the price. After reconfiguration, 3 more passes are sold, resulting in 6 total passes against a maxSupply of 3.
Output:
Guard reconfiguration so it cannot run after sales begin:
If price updates are needed after sales begin, add a separate updatePassPrice(uint256 passId, uint256 newPrice) function that does not touch supply.
# \[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.