Beatland Festival

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

configurePass() resets passSupply to zero, allowing the organizer to bypass the maximum supply cap and mint unlimited passes

Root + Impact

Description

  • FestivalPass is an ERC-1155 contract where pass types have a hard passMaxSupply cap enforced by buyPass to limit ticket availability.

  • configurePass unconditionally resets passSupply[passId] = 0 without invalidating already-minted tokens, so after a pass type sells out the organizer can call configurePass again to reset the counter and allow another full batch of mints, repeating indefinitely.

function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer {
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;
// @> resets the live mint counter — existing minted tokens are NOT burned
passSupply[passId] = 0;
emit PassConfigured(passId, price, maxSupply);
}
function buyPass(uint256 collectionId) external payable {
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
// @> supply counter was just reset to 0 — check passes again even though maxSupply were already minted
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
}

Risk

Likelihood:

  • Requires an organizer action (privileged but not unlikely — could be intentional rug or an accidental reconfiguration), and the reset is a single transaction that immediately re-opens minting.

Impact:

  • The total number of circulating passes can far exceed the advertised maximum supply, undermining ticket scarcity, venue capacity limits, and the economic value of all previously purchased passes.

Proof of Concept

An organizer sells all maxSupply passes, then calls configurePass to reset the counter and sells another full batch while all prior passes remain valid.

function test_configurePassBypassesMaxSupply() public {
uint256 passId = GENERAL_PASS;
uint256 price = 0.1 ether;
uint256 maxSup = 2;
vm.prank(organizer);
festivalPass.configurePass(passId, price, maxSup);
// Buy all passes up to maxSupply
for (uint256 i; i < maxSup; i++) {
address buyer = address(uint160(i + 1));
vm.deal(buyer, price);
vm.prank(buyer);
festivalPass.buyPass{value: price}(passId);
}
// Confirm sold out
vm.deal(address(99), price);
vm.prank(address(99));
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: price}(passId);
// Organizer resets supply — existing passes untouched
vm.prank(organizer);
festivalPass.configurePass(passId, price, maxSup); // passSupply reset to 0
// Now another maxSupply can be minted — total in circulation exceeds maxSup
vm.deal(address(100), price);
vm.prank(address(100));
festivalPass.buyPass{value: price}(passId); // succeeds — supply cap bypassed
assertGt(festivalPass.passSupply(passId) + maxSup, maxSup);
}

The test confirms that re-calling configurePass allows minting past the original supply cap.

Recommended Mitigation

Track a separate immutable total-minted counter that is never reset, or revert if passes have already been minted under the current configuration.

+ mapping(uint256 => uint256) public passTotalMinted;
function configurePass(...) external onlyOrganizer {
+ require(passSupply[passId] == 0, "Cannot reconfigure after minting has started");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0;
}
function buyPass(uint256 collectionId) external payable {
...
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
+ ++passTotalMinted[collectionId];
}
Updates

Lead Judging Commences

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