Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

configurePass unconditionally resets passSupply to 0

Summary

configurePass() unconditionally executes passSupply[passId] = 0, resetting the sold-counter every time a pass is (re)configured. An organizer can therefore reconfigure a pass after sales have occurred and silently exceed the intended maxSupply, over-minting passes. Recorded as informational: the organizer is a trusted role.

Root Cause

Contract: src/FestivalPass.sol
Function: configurePass(...)

function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer {
// ... stores price / maxSupply ...
passSupply[passId] = 0; // @> unconditional reset on every (re)configuration
}

There is no "first-time only" guard (e.g., a configured flag), so the reset fires on every call.

Impact

  • Silent supply-cap bypass: after selling passes against a maxSupply, the organizer can call configurePass again (even with identical parameters) and the sold counter restarts at 0, allowing fresh mints up to maxSupply once more — over-issuance with no on-chain trace of the reset.

  • Informational / Low: the organizer is a trusted role, so this is not counted as an exploitable vulnerability — recorded for awareness and defense-in-depth. If the role were ever compromised, misused, or handed to a less-trusted party, the supply cap could be bypassed without any code change.

Description

configurePass is the organizer's function for setting a pass tier's price and maxSupply. Its body unconditionally zeroes passSupply[passId]. The intended use is presumably one-time initialization, but nothing enforces that: any subsequent call — e.g., adjusting the price after launch — also wipes the sales counter. Combined with the buyPass sold-out check (which reads passSupply), this lets the organizer exceed the advertised maxSupply at will, breaking the "limited edition" promise without any visible anomaly.

Under the contest's trusted-organizer assumption this is informational only, but it is worth documenting so the behavior is explicit rather than accidental.

Proof of Concept

No executable PoC test — evident by source inspection: every configurePass call executes passSupply[passId] = 0 unconditionally. A Foundry test would simply configure, buy, reconfigure, and observe passSupply returning to 0; the code path is a single straight-line statement with no branching.

Recommended Mitigation

Only initialize the counter on first configuration, and document the behavior:

function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer {
// ... stores price / maxSupply ...
- passSupply[passId] = 0;
+ if (!passConfigured[passId]) {
+ passConfigured[passId] = true;
+ passSupply[passId] = 0;
+ }
}

Alternatively, keep the reset but document explicitly that reconfiguration restarts sales, so organizers act with full awareness.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-01] [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

# \[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; } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!