Beatland Festival

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

### `configurePass` resets `passSupply` to zero and ignores already-minted passes, allowing minting beyond `maxSupply`

** Description **

  • configurePass is intended to let the organizer set the price and maximum supply for a pass type. The supply check in buyPass relies on passSupply[collectionId] < passMaxSupply[collectionId] to enforce the cap.

  • Every call to configurePass unconditionally resets passSupply[passId] to 0, regardless of how many passes have already been minted and are held by users. It also does not validate that the new maxSupply is greater than or equal to the number of passes already in circulation. This means reconfiguring a pass — even just to update the price — wipes the supply counter and allows a full round of new mints on top of the passes already out there.

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
}

** Risk **

Likelihood:

  • The organizer calls configurePass a second time on a pass type that has already been sold — whether to adjust pricing, increase supply, or by mistake — and the supply counter silently resets to zero

  • A compromised or malicious organizer deliberately reconfigures passes to mint far beyond the advertised cap

Impact:

  • Total circulating passes exceed maxSupply with no on-chain record — passSupply only reflects mints since the last reconfiguration, so the contract's supply tracking is permanently inaccurate

  • Pass holders who purchased under a limited-supply guarantee are diluted, devaluing their passes and the associated BEAT reward earnings

** Proof of Concept **

function test_ConfigurePass_ResetsSupplyAllowsExcessMinting() public {
// Configure GENERAL pass with max supply of 2
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
// user1 and user2 each buy a pass — supply is now 2/2 (full)
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(user2);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
assertEq(festivalPass.passSupply(1), 2);
// A third buyer is correctly blocked
address user3 = makeAddr("user3");
vm.deal(user3, 10 ether);
vm.prank(user3);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Organizer reconfigures the same pass (e.g. to update price)
// This resets passSupply to 0 even though 2 passes are already circulating
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
// passSupply now reads 0, but 2 users are already holding passes
assertEq(festivalPass.passSupply(1), 0);
assertEq(festivalPass.balanceOf(user1, 1), 1);
assertEq(festivalPass.balanceOf(user2, 1), 1);
// user3 and a fourth user can now buy — total circulating becomes 4, double the maxSupply
address user4 = makeAddr("user4");
vm.deal(user4, 10 ether);
vm.prank(user3);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(user4);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// 4 passes in circulation with a maxSupply of 2
assertEq(festivalPass.balanceOf(user1, 1), 1);
assertEq(festivalPass.balanceOf(user2, 1), 1);
assertEq(festivalPass.balanceOf(user3, 1), 1);
assertEq(festivalPass.balanceOf(user4, 1), 1);
assertEq(festivalPass.passSupply(1), 2); // contract thinks only 2 exist
}

** Recommended Mitigation **

Remove the supply reset and add a validation that the new maxSupply cannot be set below the number of passes already minted:

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], "New max supply below current supply");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 10 days 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!