Beatland Festival

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

configurePass() unconditionally resets passSupply to zero, allowing unlimited pass minting beyond the advertised cap

Root + Impact

configurePass() combines configuration and supply-reset in a single organizer-accessible call. There is no invariant preventing reconfiguration after minting has begun. The supply cap is purely cosmetic — the organizer can bypass it at will, inflating supply of any pass tier to an arbitrary number.

Description

Every call to configurePass() contains:

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

This unconditionally resets the minted count to zero regardless of how many passes have already been sold. An organizer — or anyone who compromises the organizer key — can:

  1. Sell all passes up to maxSupply, collecting ETH.

  2. Call configurePass(passId, price, maxSupply) to reset passSupply[passId] back to 0.

  3. Repeat indefinitely, minting the same token IDs again while collecting fresh ETH each round.

Because ERC1155 allows multiple owners to hold the same token ID, existing pass holders are not ejected — but the total supply of that token ID grows without bound, destroying scarcity guarantees.

Risk

// Round 1
festivalPass.configurePass(1, 0.1 ether, 100);
// ... 100 users buy passes, organizer collects 10 ETH ...
// Attacker reconfigures — resets supply counter
festivalPass.configurePass(1, 0.1 ether, 100); // passSupply[1] = 0 again
// Round 2: 100 more passes sold against the same cap
// Total supply of token ID 1 = 200, advertised cap = 100
// Repeatable indefinitely in the same block

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "../src/FestivalPass.sol";
import "../src/BeatToken.sol";
contract ConfigurePassResetTest is Test {
FestivalPass fp;
BeatToken bt;
address organizer = address(0xB0B);
address alice = address(0xA);
address bob = address(0xB);
function setUp() public {
bt = new BeatToken();
fp = new FestivalPass(address(bt), organizer);
bt.setFestivalContract(address(fp));
}
function test_unlimitedMintViaReset() public {
// Organizer configures pass: cap = 1
vm.prank(organizer);
fp.configurePass(1, 0.1 ether, 1);
// Alice buys the only allowed pass
vm.deal(alice, 1 ether);
vm.prank(alice);
fp.buyPass{value: 0.1 ether}(1);
assertEq(fp.passSupply(1), 1);
// Organizer resets supply by reconfiguring
vm.prank(organizer);
fp.configurePass(1, 0.1 ether, 1);
assertEq(fp.passSupply(1), 0); // RESET — cap bypassed
// Bob buys another pass beyond the cap
vm.deal(bob, 1 ether);
vm.prank(bob);
fp.buyPass{value: 0.1 ether}(1);
// Both hold token ID 1; total supply = 2, cap was 1
assertEq(fp.balanceOf(alice, 1), 1);
assertEq(fp.balanceOf(bob, 1), 1);
}
}

Recommended Mitigation

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 minting has begun");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // DELETE this line
}
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!