Beatland Festival

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

configurePass() resets passSupply to 0 allowing unlimited oversupply beyond maxSupply

Description

The configurePass() function contains a critical supply tracking bug that allows the total circulating supply of festival passes to exceed the configured maxSupply limit indefinitely.

Root cause: The function unconditionally resets passSupply[passId] = 0 regardless of existing minted passes:

Attack vector:

  1. Users buy passes → passSupply increases (e.g., 8/10 minted)

  2. Organizer calls configurePass() (normal price adjustment)

  3. passSupply resets to 0 → contract "forgets" existing mints

  4. Users buy new passes up to full maxSupply again

  5. Result: 8 (old) + 12 (new) = 20 passes when maxSupply = 12

This breaks the core economic invariant: total minted supply ≤ maxSupply.

Risk

this might cause great trouble for the organizer as the total passes in the circulation will exceeds the total supply.

Proof of Concept

Test file passes and demonstrates the full exploit:

solidity

function test_ConfigurePassResetsSupplyAndAllowsOversupply() public {
// Step 1: Organizer configures VIP pass with max supply of 10
vm.prank(organizer);
festival.configurePass(VIP_PASS, 1 ether, 10);
assertEq(festival.passPrice(VIP_PASS), 1 ether);
assertEq(festival.passMaxSupply(VIP_PASS), 10);
assertEq(festival.passSupply(VIP_PASS), 0);
// Step 2: 8 users buy VIP passes → current supply becomes 8
for (uint160 i = 1; i <= 8; i++) {
address buyer = address(uint160(0x1000 + i));
vm.deal(buyer, 2 ether);
vm.prank(buyer);
festival.buyPass{value: 1 ether}(VIP_PASS);
}
assertEq(festival.passSupply(VIP_PASS), 8, "Should have 8 passes minted");
assertEq(festival.passMaxSupply(VIP_PASS), 10, "Max supply still 10");
// Step 3: Organizer re-configures the pass (e.g., lowers price or changes maxSupply)
vm.prank(organizer);
festival.configurePass(VIP_PASS, 0.5 ether, 12); // New price + increased maxSupply
// CRITICAL BUG: passSupply has been reset to 0!
assertEq(festival.passSupply(VIP_PASS), 0, "Supply was incorrectly reset to 0");
// Step 4: Now new buyers can purchase up to the full new maxSupply (12)
// This means 12 NEW passes can be minted, on top of the 8 that already exist
for (uint160 i = 9; i <= 20; i++) { // 12 new buyers
address buyer = address(uint160(0x1000 + i));
vm.deal(buyer, 1 ether);
vm.prank(buyer);
festival.buyPass{value: 0.5 ether}(VIP_PASS);
}
address finalbuyer = address(0x1212121212);
vm.prank(finalbuyer);
vm.deal(finalbuyer,1 ether);
vm.expectRevert();
festival.buyPass{value : 0.5 ether}(VIP_PASS);
// Final state:
// Contract thinks only 12 passes have been minted
assertEq(festival.passSupply(VIP_PASS), 12, "Contract reports only 12 minted");
// But in reality, 8 (before reset) + 12 (after reset) = 20 passes exist
uint256 actualCirculating = 0;
for (uint160 i = 1; i <= 20; i++) {
address holder = address(uint160(0x1000 + i)); // First 8
actualCirculating+=festival.balanceOf(holder, VIP_PASS);
}
assertEq(actualCirculating, 20, "Actual circulating VIP passes = 20");
// The declared maxSupply is only 12 → invariant completely broken
assertGt(actualCirculating, festival.passMaxSupply(VIP_PASS), "Circulating supply EXCEEDS maxSupply");
}
}

Test Results:

text

[PASS] test_ConfigurePassResetsSupplyAndAllowsOversupply() (gas: 1496319)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.33ms (1.30ms CPU time)

Exploit Scenarios

  1. Price adjustment (most likely): Organizer lowers VIP price → accidentally enables 2x supply

  2. MaxSupply increase: From 10→20, but gets 30 total passes instead

  3. Repeated abuse: Configure → sell → reconfigure → sell → infinite supply

Recommended mitigation

Immediate fix - Remove the reset line entirely:

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

Lead Judging Commences

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