Beatland Festival

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

configurePass Unconditionally Resets passSupply to Zero, Allowing Minting Beyond maxSupply

Description

  • The organizer calls configurePass to set a pass tier's price and maximum supply before sales begin. Pass holders cannot exceed the configured maxSupply because
    buyPass checks passSupply[collectionId] < passMaxSupply[collectionId].

  • configurePass sets passSupply[passId] = 0 on every invocation, including routine reconfiguration calls made after passes are already in circulation. An
    organizer who simply updates a pass price after sales begin inadvertently resets the minted counter to zero, making the supply check treat the tier as having
    zero existing passes and allowing the full maxSupply quota to be sold again on top of however many are already in wallets.

// src/FestivalPass.sol

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 — executes unconditionally, even mid-sale
}

Risk

Likelihood:

  • Every time the organizer needs to adjust pass pricing — a routine operational task — the supply counter is silently reset with no warning, no guard, and no
    documentation of this side effect.

  • The comment // Reset current supply suggests the developer intended this for fresh configurations but did not account for mid-sale reconfiguration being a
    normal real-world need.

Impact:

  • Total circulating passes can grow to any multiple of maxSupply with each reconfigure-and-resell cycle, breaking the scarcity guarantee advertised to buyers who
    paid a premium for limited-edition passes.

  • Every BACKSTAGE re-mint yields a 15e18 BEAT welcome bonus and 3× multiplier, compounding BEAT token inflation with every cycle.

Proof of Concept

function test_ConfigurePassSupplyReset() public {
// Configure GENERAL pass with maxSupply = 2
vm.prank(organizer);
festivalPass.configurePass(1, 0.05 ether, 2);

  // Sell both passes — supply is now full              
  vm.prank(user1);                                                                                                                                             
  festivalPass.buyPass{value: 0.05 ether}(1);           
  vm.prank(user2);                                                                                                                                             
  festivalPass.buyPass{value: 0.05 ether}(1);
                                                                                                                                                               
  // Third purchase correctly reverts                                                                                                                          
  address user3 = makeAddr("user3");
  vm.deal(user3, 1 ether);                                                                                                                                     
  vm.prank(user3);                                      
  vm.expectRevert("Max supply reached");                                                                                                                       
  festivalPass.buyPass{value: 0.05 ether}(1);
                                                                                                                                                               
  // Organizer updates price (routine task) → supply counter silently reset to 0                                                                               
  vm.prank(organizer);
  festivalPass.configurePass(1, 0.06 ether, 2);                                                                                                                
                                                        
  // Third purchase now succeeds — 3 passes exist against a cap of 2                                                                                           
  vm.prank(user3);
  festivalPass.buyPass{value: 0.06 ether}(1);                                                                                                                  
  assertEq(festivalPass.balanceOf(user3, 1), 1);        
                                                                                                                                                               
  // passSupply[1] == 1 (post-reset counter), but 3 ERC1155 tokens exist                                                                                       
  assertEq(festivalPass.passSupply(1), 1);                                                                                                                     

}

Recommended Mitigation

// src/FestivalPass.sol

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 pass with existing supply");

    passPrice[passId] = price;
    passMaxSupply[passId] = maxSupply;

  • passSupply[passId] = 0; // Reset current supply
    }

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!