Beatland Festival

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

# H-2: `configurePass` resets on-chain supply counter and bypasses pass sale cap

Root + Impact

Pass sales are capped using a mutable counter passSupply, not the actual number of ERC1155 tokens in circulation. When the organizer calls configurePass again, passSupply is reset to zero, allowing additional mints even after the original cap was reached.

Description

  • Normal behavior: each pass tier (General, VIP, Backstage) has a maximum number of passes that can ever be sold (passMaxSupply), and buyPass increments passSupply until the cap is hit.

  • configurePass always sets passSupply[passId] = 0 without checking how many passes were already minted. A second configuration round re-opens sales up to maxSupply again, so total minted passes can exceed the intended festival cap.

// src/FestivalPass.sol
function configurePass(
uint256 passId,
uint256 price,
uint256 maxSupply
) external onlyOrganizer {
// ...
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
// @> Resets counter; ignores existing ERC1155 balances
passSupply[passId] = 0;
}
function buyPass(uint256 collectionId) external payable {
// ...
// @> Gate uses passSupply only, not sum(balanceOf(all users))
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
}

Risk

Likelihood:

  • The organizer calls configurePass again after initial sell-out (price update, mistake, or malicious intent).

  • The README states the organizer is trusted; this path is still a one-transaction cap break with no on-chain enforcement of total lifetime supply.

Impact:

  • More pass NFTs exist than advertised (e.g. 5000 cap → 10000+ minted).

  • Extra ETH revenue to the owner, but broken scarcity and unfairness for early buyers.

  • Combined with H-1, extra passes amplify BEAT farming.

Proof of Concept

function test_H02_configurePass_resetsSupplyCounter() public {
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(user2);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Cap reached: passSupply == 2, two NFTs outstanding
assertEq(festivalPass.passSupply(1), 2);
// Organizer re-configures same tier; counter reset to 0
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 2);
// user1 buys again — allowed because passSupply was reset
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
assertEq(festivalPass.passSupply(1), 1);
assertEq(festivalPass.balanceOf(user1, 1), 2); // user1 holds 2 General passes; cap was 2 total
}

Recommended Mitigation

Never reset passSupply on reconfiguration; derive availability from minted total or enforce monotonic cap:

function configurePass(uint256 passId, uint256 price, uint256 maxSupply) external onlyOrganizer {
passPrice[passId] = price;
+ require(maxSupply >= passSupply[passId], "Cannot lower cap below minted");
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0;
}

Or replace passSupply with _totalMinted(passId) that cannot be decremented.

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!