Beatland Festival

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

Supply counter reset in `configurePass` allows unlimited minting beyond maximum supply limits

Description

The FestivalPass::configurePass function unconditionally resets the passSupply[passId] counter to 0, regardless of how many passes have already been minted and are currently in circulation. This creates a critical accounting mismatch between the actual number of NFTs in circulation and the contract's internal supply tracking.

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 - VULNERABILITY
}

The buyPass function relies on passSupply[collectionId] < passMaxSupply[collectionId] to enforce supply limits. When the supply counter is reset to 0, users can mint up to the full maxSupply amount again, completely bypassing the intended scarcity mechanism.

Impact

  • Supply Limit Bypass: Users can mint significantly more passes than the intended maximum supply, potentially doubling or tripling the total circulation

  • Economic Fraud: Holders who paid for "limited edition" passes are defrauded as their passes lose scarcity value

  • Revenue Loss: The protocol may lose revenue from artificially deflated pass prices due to oversupply

  • Trust Violation: Breaks fundamental promises about NFT scarcity and maximum supply limits

  • Market Manipulation: Malicious organizers can intentionally flood the market by repeatedly calling configurePass

Proof of Concept

function testMaxSupplyBypassThroughSupplyReset() public {
// Initial state: Max supply of 100 backstage passes
console.log("Total max supply of Backstage pass is ", festivalPass.passMaxSupply(3));
console.log("Total current supply of Backstage pass is ", festivalPass.passSupply(3));
// User1 buys 50 backstage passes (50% of max supply)
vm.startPrank(user1);
for (uint256 i = 0; i < 50; i++) {
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
}
vm.stopPrank();
assertEq(festivalPass.balanceOf(user1, 3), 50);
assertEq(festivalPass.passSupply(3), 50);
console.log("Total current supply after user purchased: ", festivalPass.passSupply(3));
// Organizer reconfigures the pass (e.g., to update price)
vm.prank(organizer);
uint256 passId = 3;
uint256 newPrice = 0.5 ether;
uint256 newMaxSupply = 200;
festivalPass.configurePass(passId, newPrice, newMaxSupply);
console.log("New Total max supply of Backstage pass is ", festivalPass.passMaxSupply(3));
console.log("New Total current supply of Backstage pass is ", festivalPass.passSupply(3));
console.log("But User has 50 Backstage passes even though current circulation shows 0");
// VULNERABILITY: Supply counter reset to 0 despite 50 passes in circulation
assertTrue(festivalPass.passSupply(3) == 0); // Reset to 0
assertEq(festivalPass.balanceOf(user1, 3), 50); // User still owns 50 passes
// Now users can buy up to the full maxSupply again (200 more passes)
// Total possible circulation: 50 (existing) + 200 (new) = 250 passes
// This breaks the intended maximum supply mechanism
}

Logs:

Total max supply of Backstage pass is 100
Total current supply of Backstage pass is 0
Total current supply after user purchased: 50
New Total max supply of Backstage pass is 200
New Total current supply of Backstage pass is 0
But User has 50 Backstage passes even though current circulation shows 0

Recommended Mitigation:

Remove the supply counter reset and add validation to prevent reducing max supply below current circulation:

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(maxSupply >= passSupply[passId], "Cannot set max supply below current supply");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}

This ensures that:

  1. The supply counter accurately reflects the actual number of passes in circulation

  2. Maximum supply cannot be reduced below the current circulating supply

  3. The scarcity mechanism works as intended

  4. Existing pass holders' investments are protected

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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.