Beatland Festival

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

[H-03] `configurePass` resets `passSupply` to zero, breaking supply cap after any reconfiguration

Description

configurePass unconditionally sets passSupply[passId] = 0 at line 65. If the organizer calls this function after passes have already been sold (e.g., to update the price for a second wave), the supply counter loses all previously sold passes. The maxSupply check in buyPass then allows minting a full second batch, putting more passes into circulation than maxSupply intended.

Vulnerability Details

// src/FestivalPass.sol, lines 54-66
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; // @> unconditionally resets — erases all sold passes from the counter
}

The supply reset is unconditional. There is no check for passSupply[passId] > 0 and no way to update just the price without also resetting the supply. Even an honest organizer who simply wants to change the price of a pass tier will inadvertently reset the supply counter.

Consider the following scenario:

  1. Organizer configures GENERAL pass: price = 1 ETH, maxSupply = 3

  2. Three users buy passes. passSupply[GENERAL_PASS] = 3. Next buyer gets "Max supply reached".

  3. Organizer wants to raise the price to 1.5 ETH for a "second wave" but keep the same maxSupply of 3

  4. Organizer calls configurePass(1, 1.5 ether, 3). passSupply resets to 0.

  5. Three more users buy passes at 1.5 ETH each. passSupply = 3 again.

  6. Now 6 GENERAL passes exist in circulation, but maxSupply was 3.

This is not an admin-trust issue because the organizer is not acting maliciously. They are using configurePass for its intended purpose (updating price), and the supply reset is an unintended side effect with no way to avoid it.

Risk

Likelihood:

  • Reconfiguring a pass is a normal operational action (adjusting pricing based on demand, extending a sale). The function provides no way to update price without resetting supply, so any reconfiguration triggers the bug.

Impact:

  • Pass scarcity is the core value proposition of the tiered system. Backstage passes with a maxSupply of 50 could have 200 in circulation after a few reconfigurations. Existing holders suffer value dilution. The 3x BEAT multiplier for Backstage means each extra pass generates outsized BEAT rewards that drain memorabilia collections faster than intended.

Proof of Concept

The test configures GENERAL pass with maxSupply=3, sells 3 passes, then reconfigures to update the price. After reconfiguration, 3 more passes are sold, resulting in 6 total passes against a maxSupply of 3.

function testExploit_ConfigurePassReset() public {
// Configure GENERAL pass: 1 ETH, max 3
vm.prank(organizer);
festivalPass.configurePass(1, 1 ether, 3);
// Alice buys 3 GENERAL passes (fills maxSupply)
vm.deal(alice, 3 ether);
for (uint i = 0; i < 3; i++) {
vm.prank(alice);
festivalPass.buyPass{value: 1 ether}(1);
}
uint256 supplyAfterSoldOut = festivalPass.passSupply(1);
assertEq(supplyAfterSoldOut, 3, "Supply should be 3 (sold out)");
// Bob tries to buy — should fail (sold out)
vm.deal(bob, 1 ether);
vm.prank(bob);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: 1 ether}(1);
// Organizer reconfigures pass to update price — passSupply resets to 0!
vm.prank(organizer);
festivalPass.configurePass(1, 1.5 ether, 3);
uint256 supplyAfterReconfig = festivalPass.passSupply(1);
assertEq(supplyAfterReconfig, 0, "Supply reset to 0 after reconfigure!");
// Bob can now buy 3 more passes — 6 total in circulation
vm.deal(bob, 4.5 ether);
for (uint i = 0; i < 3; i++) {
vm.prank(bob);
festivalPass.buyPass{value: 1.5 ether}(1);
}
uint256 totalPasses = festivalPass.balanceOf(alice, 1) + festivalPass.balanceOf(bob, 1);
assertEq(totalPasses, 6, "EXPLOIT PROVEN: 6 passes exist with maxSupply of 3");
assertEq(festivalPass.passSupply(1), 3, "passSupply only tracks 3 (lost track of first batch)");
}

Output:

Alice passes: 3
Bob passes: 3
Total passes in circulation: 6
passSupply (tracked): 3
maxSupply: 3

Recommendations

Guard reconfiguration so it cannot run after sales begin:

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(passSupply[passId] == 0, "Cannot reconfigure after sales begin");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0;
}

If price updates are needed after sales begin, add a separate updatePassPrice(uint256 passId, uint256 newPrice) function that does not touch supply.

Updates

Lead Judging Commences

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