Beatland Festival

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

`configurePass` Resets Supply Counter — Organizer Can Mint Unlimited Passes

configurePass Resets Supply Counter — Organizer Can Mint Unlimited Passes

Scope

  • FestivalPass.sol

Description

  • The configurePass() function is intended to allow the organizer to update pass pricing and maximum supply. It should not affect already-sold passes.

  • The function unconditionally resets passSupply[passId] = 0 (line 65) regardless of how many passes have already been sold. This allows the organizer to call configurePass() after maxSupply passes are sold, resetting the counter and enabling additional sales beyond the original limit. Total circulating passes exceed maxSupply.

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 — destroys supply tracking
}

Risk

Likelihood: Medium

  • Requires organizer privilege. A malicious or compromised organizer can exploit this. An honest organizer may trigger it accidentally when adjusting prices.

Impact: High

  • Unlimited passes can be minted, destroying scarcity guarantees. BACKSTAGE pass holders get 3x BEAT rewards — with unlimited passes, BEAT token inflation is unbounded.

Severity: High

Proof of Concept

All 10 GENERAL passes are sold. The organizer calls configurePass() to update the price, which resets passSupply to 0. Now 10 more passes can be sold, resulting in 20 total passes in circulation despite maxSupply = 10.

function test_F3_supplyReset_configurePass() public {
vm.deal(alice, 100 ether);
vm.startPrank(alice);
for (uint256 i = 0; i < 10; i++) {
festival.buyPass{value: 0.1 ether}(1); // Buy all 10 GENERAL
}
vm.stopPrank();
assertEq(festival.passSupply(1), 10, "All 10 passes sold");
vm.prank(organizer);
festival.configurePass(1, 0.1 ether, 10); // RESETS supply to 0
assertEq(festival.passSupply(1), 0, "Supply reset to 0 after reconfigure");
vm.deal(bob, 100 ether);
vm.prank(bob);
festival.buyPass{value: 0.1 ether}(1); // Works! 11th pass
uint256 totalPasses = festival.balanceOf(alice, 1) + festival.balanceOf(bob, 1);
assertGt(totalPasses, 10, "More passes exist than original maxSupply");
// totalPasses = 11; maxSupply was 10
}

PoC result: test_F3_supplyReset_configurePass()PASS (gas: 671,337).

Recommended Mitigation

Don't reset the supply counter during reconfiguration. If supply reduction is needed, add explicit logic.

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 must be >= current supply");
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!