Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Organizer can bypass max supply limits by resetting pass supply counters

Description

Normal behavior: Pass supply tracking should prevent sales beyond configured maxSupply limits. Once passes are sold, the supply counter should accurately reflect total minted passes.

Issue: The configurePass() function resets passSupply[passId] = 0 on line 65, allowing the organizer to bypass max supply limits. Users keep their existing passes, but the supply counter resets, enabling overselling beyond intended limits.

Root Cause

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; // @> Always resets supply to 0, ignoring existing passes
}

The function unconditionally resets the supply counter without considering already-minted passes.

Risk

Likelihood:

  • Organizer can trigger this anytime by reconfiguring pass parameters

  • No restrictions prevent supply reset during active sales periods

Impact:

  • Supply limit bypass: Allows selling beyond intended scarcity limits

  • Economic manipulation: "Limited edition" passes become unlimited

  • User deception: Buyers believe they own scarce assets that aren't actually limited

Proof of Concept

function test_PassSupplyResetAttack() public {
// Users buy passes (2 backstage passes sold)
vm.startPrank(user1);
festivalPass.buyPass{value: 1 ether}(3); // Backstage pass
vm.stopPrank();
vm.startPrank(user2);
festivalPass.buyPass{value: 1 ether}(3); // Another backstage pass
vm.stopPrank();
// Supply shows 2 passes sold
assertEq(festivalPass.passSupply(3), 2);
// Organizer reconfigures pass (resets supply to 0)
vm.startPrank(organizer);
festivalPass.configurePass(3, 1 ether, 20);
vm.stopPrank();
// Supply counter reset to 0, but users still own their passes
assertEq(festivalPass.passSupply(3), 0);
assertEq(festivalPass.balanceOf(user1, 3), 1); // User1 still has pass
assertEq(festivalPass.balanceOf(user2, 3), 1); // User2 still has pass
// Now organizer can sell 20 more passes despite 2 already existing
}

Result: Supply tracking bypassed - 22 total passes can exist despite 20 max supply limit.

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 pass with existing supply");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}

This prevents reconfiguration after passes have been sold, maintaining supply integrity.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

configurePass resets the current pass supply circumventing the max supply check

This is not acceptable as high because any attack vectors related to organizer trying to milk ETH from participants is voided by the fact that the organizer is trusted.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.