Beatland Festival

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

Organizer can reset pass supply counter to mint unlimited passes beyond advertised max supply

Organizer can reset pass supply counter to mint unlimited passes beyond advertised max supply

Description

The FestivalPass::configurePass function allows the organizer to reconfigure pass settings at any time. When called, it resets the passSupply counter to zero, regardless of how many passes have already been minted.

This allows the organizer to bypass the maxSupply limit by simply calling configurePass again after the supply is exhausted, enabling unlimited minting of passes that were advertised as "limited edition".

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
}

Risk

Likelihood:

  • This will occur when the organizer calls configurePass after passes have already been sold, intentionally or accidentally resetting the supply counter.

Impact:

  • Users who purchased "limited edition" passes (e.g., 100 max supply BACKSTAGE passes) are defrauded as unlimited passes can be minted, destroying the scarcity value they paid a premium for.

  • Trust in the platform is undermined as advertised supply limits are not enforced on-chain.

Proof of Concept

  1. Organizer configures BACKSTAGE pass with maxSupply of 100

  2. All 100 passes are sold to users expecting scarcity

  3. Organizer calls configurePass again with the same parameters

  4. passSupply is reset to 0, allowing another 100 passes to be sold

  5. Repeat indefinitely

Add the following test to your FestivalPass.t.sol file:

function testOrganizerCanResetSupplyToMintUnlimitedPasses() public {
// Initial supply is 100 for backstage
assertEq(festivalPass.passMaxSupply(3), BACKSTAGE_MAX_SUPPLY);
// Simulate all 100 passes being sold
for (uint256 i = 0; i < BACKSTAGE_MAX_SUPPLY; i++) {
address buyer = makeAddr(string(abi.encodePacked("buyer", i)));
vm.deal(buyer, BACKSTAGE_PRICE);
vm.prank(buyer);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
}
// Supply is now maxed out
assertEq(festivalPass.passSupply(3), BACKSTAGE_MAX_SUPPLY);
// Organizer reconfigures - this resets supply to 0
vm.prank(organizer);
festivalPass.configurePass(3, BACKSTAGE_PRICE, BACKSTAGE_MAX_SUPPLY);
// Supply is reset, can mint 100 more
assertEq(festivalPass.passSupply(3), 0);
// Mint another 100 passes - total now 200 despite "100 max supply"
for (uint256 i = 0; i < BACKSTAGE_MAX_SUPPLY; i++) {
address buyer = makeAddr(string(abi.encodePacked("buyer2", i)));
vm.deal(buyer, BACKSTAGE_PRICE);
vm.prank(buyer);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
}
// 200 passes now exist despite 100 max supply
assertEq(festivalPass.balanceOf(makeAddr(string(abi.encodePacked("buyer", uint256(0)))), 3), 1);
assertEq(festivalPass.balanceOf(makeAddr(string(abi.encodePacked("buyer2", uint256(0)))), 3), 1);
}

Recommended Mitigation

Remove the supply reset, or only allow configuration before any passes are sold:

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 sales have started");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}
Updates

Lead Judging Commences

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