Beatland Festival

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

Unlimited-Mint via configurePass Resets Minted Counter

Root + Impact

Description

configurePass() lets the organizer change pricing or limits for any pass collection.
Unfortunately it also zeroes the on-chain minted counter that the primary mint function relies on to enforce the cap:

function configurePass(
uint256 collectionId,
uint256 newPrice,
uint256 newMaxSupply
) external onlyOwner {
passPrice[collectionId] = newPrice;
passMaxSupply[collectionId] = newMaxSupply;
passSupply[collectionId] = 0; // @> resets minted counter
}

The sales routine trusts passSupply[collectionId] to block over-minting:

function buyPass(uint256 collectionId) external payable {
...
require(passSupply[collectionId] < passMaxSupply[collectionId],
"Max supply reached");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
}

After an event is live and hundreds of passes are already in circulation, the owner can call configurePass() again.
Because passSupply is set back to 0, every subsequent call to buyPass() thinks no tokens have been sold and happily mints up to the cap again, allowing the total supply to grow without bound.
This breaks scarcity, undermines resale value, and inflates any pass-based reward calculations.

Risk

Likelihood:

Whenever the organizer calls configurePass for a pass that was already configured.

Impact:

Economic & reputational: existing pass-holders are diluted, secondary-market pricing collapses, and any “per-pass” perks (BEAT bonuses, backstage access, leaderboards) lose integrity.

Proof of Concept

  1. Mint 10 general passes (counter now = 10).

  2. configurePass(general, samePrice, 5) → passSupply = 0.

  3. Call buyPass(general) five more times.
    All succeed even though actual circulating supply is now 15 > cap.

function test_SupplyCanBeMintedTwice() public {
uint256 passId = 1; // General pass
uint256 initialCap = 10; // original maxSupply
uint256 newCap = 5; // new cap (resets counter)
/*────────────────── SETUP: configure pass with initial cap ─────────────────*/
vm.startPrank(organizer);
festivalPass.configurePass(passId, GENERAL_PRICE, initialCap);
vm.stopPrank();
/*────────────────── USER mints up to the original cap ─────────────────────*/
vm.deal(user1, GENERAL_PRICE * initialCap); // fund user
vm.startPrank(user1);
for (uint256 i; i < initialCap; ++i) {
festivalPass.buyPass{value: GENERAL_PRICE}(passId);
}
vm.stopPrank();
/*────────────────── ORGANISER resets counter via configurePass ─────────────*/
vm.startPrank(organizer);
festivalPass.configurePass(passId, GENERAL_PRICE, newCap); // passSupply = 0
vm.stopPrank();
/*────────────────── USER mints again beyond the intended total cap ─────────*/
vm.deal(user1, GENERAL_PRICE * newCap); // refuel ETH
vm.startPrank(user1);
for (uint256 i; i < newCap; ++i) {
festivalPass.buyPass{value: GENERAL_PRICE}(passId);
}
vm.stopPrank();
/*────────────────── ASSERT: total passes > original cap (10 → 15) ─────────*/
uint256 finalBalance = festivalPass.balanceOf(user1, passId);
assertEq(finalBalance, initialCap + newCap); // 15
assertGt(finalBalance, initialCap); // > 10 cap breached
}

Recommended Mitigation

  • Never touch passSupply inside configurator functions.

  • If the cap itself must be reduced, require newMaxSupply ≥ passSupply to avoid locking future sales.

  • Alternatively make passMaxSupply immutable after the first mint, and expose a separate “phase” mechanism for price changes.

+ require(newMaxSupply >= passSupply[collectionId], "below minted");
- passSupply[collectionId] = 0;
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months 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.