Beatland Festival

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

Resetting `passSupply` to 0 in `configurePass` allows users to bypass the max supply cap of a pass

Description

  • Normal: The configurePass function should allow the organizer to update pass parameters (price, max supply) without affecting the integrity of already-sold passes. The total number of passes minted must never exceed the configured passMaxSupply.

  • Bug: configurePass unconditionally resets passSupply[passId] = 0 on every call. If the organizer reconfigures a pass after passes have already been sold, the sold count is erased. Subsequent buyers can mint passes as if none were sold before, allowing the total minted supply to exceed passMaxSupply.

// FestivalPass.sol:57-67
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; //@> Resets sold count — erases history of already-minted passes
}

Attack chain:

  1. Organizer configures VIP pass: maxSupply = 2

  2. User1 buys pass → passSupply = 1

  3. User2 buys pass → passSupply = 2 (maxSupply reached)

  4. User3 tries to buy → reverts "Max supply reached" ✓

  5. Organizer reconfigures (e.g., adjusts price from 0.1 to 0.15 ETH)

  6. passSupply reset to 0 — 2 already-minted passes are "forgotten"

  7. User3 buys pass → passSupply = 1 (check passes: 1 < 2 ✓)

  8. User4 buys pass → passSupply = 2 (check passes: 2 < 2 ✗)

  9. Total minted: 4 passes. maxSupply: 2. Invariant broken.

Risk

  • Likelihood: Medium — Requires the organizer to call configurePass after passes have been sold. While the organizer is a trusted role, this can happen unintentionally (e.g., adjusting price parameters without realizing the supply reset side effect). The configurePass function has no warning or protection against this scenario.

  • Impact: HIGH — The max supply cap (a core scarcity invariant) is completely bypassed. Pass scarcity and value are destroyed, directly undermining the economic model. The passSupply state variable loses all meaning as an accounting mechanism.

Proof of Concept

function test_SupplyCapBypassVulnerability() public {
// Step 1: Configure pass with maxSupply = 2
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
// Step 2: Buy 2 passes (reaching maxSupply)
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(user2);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
assertEq(festivalPass.passSupply(1), 2);
assertEq(festivalPass.passMaxSupply(1), 2);
// Step 3: Third user can't buy — maxSupply correctly enforced
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, passSupply reset to 0
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
// Step 5: User3 and User4 can now buy — supply cap bypassed
vm.prank(user3);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
address user4 = makeAddr("user4");
vm.deal(user4, 10 ether);
vm.prank(user4);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Step 6: Verify the invariant violation
assertEq(festivalPass.passSupply(1), 2); // Counter says 2
assertEq(festivalPass.passMaxSupply(1), 2); // Max supply is 2
// But 4 passes were actually minted!
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 (4) exceeds maxSupply (2)!");
}

Recommended Mitigation

Remove the passSupply reset and add a validation to prevent lowering maxSupply below the current supply:

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");
+ require(maxSupply >= passSupply[passId], "maxSupply below current supply");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0;
+ // passSupply is preserved — maintains integrity of already-sold count
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 20 hours 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!