Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

configurePass() allows setting maxSupply below currently minted supply, breaking core token invariant

Description

The configurePass() function permits the organizer to decrease maxSupply to a value lower than the number of passes already minted and in circulation.

There is no validation to ensure that the new maxSupply is at least equal to the current passSupply\[passId]. This directly violates the fundamental token invariant:

Total circulating supply must never exceed the declared maximum supply

Even without considering the separate critical bug of resetting passSupply to 0, this lack of check alone allows the declared maxSupply to fall below the actual number of existing passes — rendering the cap meaningless and misleading.

Risk

  • High severity: Breaks a core economic and security invariant of capped token supplies.

  • Misleading protocol state: Users and frontends reading passMaxSupply will believe fewer passes can exist than actually do.

  • Loss of trust: VIP and Backstage passes are marketed as limited/rare — this undermines scarcity guarantees.

  • Centralized risk: Only the organizer can trigger this, but it represents a critical design flaw with economic consequences.

  • Compounds with supply reset bug: When combined with the passSupply = 0 reset (separate finding), it enables unlimited minting.

Proof of Concept

The following Foundry test passes on the current code and clearly demonstrates the invariant violation:

solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import "forge-std/Test.sol";
import "../src/FestivalPass.sol";
import "../src/BeatToken.sol";
contract FestivalPassConfigureMaxSupplyTest is Test {
FestivalPass public festival;
BeatToken public beatToken;
address public organizer = address(0x1111);
uint256 constant VIP_PASS = 2;
function setUp() public {
beatToken = new BeatToken();
festival = new FestivalPass(address(beatToken), organizer);
beatToken.setFestivalContract(address(festival));
}
/**
* @dev Test: Organizer can lower maxSupply BELOW already minted supply
* AND the supply counter is reset to 0 → double vulnerability
*/
function test_ConfigurePassAllowsLoweringMaxSupplyBelowMinted() public {
// Step 1: Configure VIP pass with maxSupply = 10
vm.prank(organizer);
festival.configurePass(VIP_PASS, 1 ether, 10);
// Step 2: Mint 7 VIP passes
for (uint160 i = 1; i <= 7; i++) {
address buyer = address(uint160(0x1000 + i));
vm.deal(buyer, 2 ether);
vm.prank(buyer);
festival.buyPass{value: 1 ether}(VIP_PASS);
}
assertEq(festival.passSupply(VIP_PASS), 7, "7 passes should be minted");
assertEq(festival.passMaxSupply(VIP_PASS), 10, "Initial maxSupply = 10");
// Step 3: Organizer lowers maxSupply to 5 (below current minted 7!)
// This should be impossible — but current code allows it
vm.prank(organizer);
festival.configurePass(VIP_PASS, 0.8 ether, 5);
// BUG 1: maxSupply is now lower than actual minted supply
assertEq(festival.passMaxSupply(VIP_PASS), 5, "maxSupply lowered to 5");
// Actual circulating supply is still 7 (proven by balances)
uint256 actualMinted = 0;
for (uint160 i = 1; i <= 7; i++) {
address holder = address(uint160(0x1000 + i));
actualMinted += festival.balanceOf(holder, VIP_PASS);
}
assertEq(actualMinted, 7, "7 passes still exist in circulation");
// INVARIANT BROKEN: actual minted (7) > declared maxSupply (5)
assertGt(actualMinted, festival.passMaxSupply(VIP_PASS), "invariantBROKEN");
}
}

Test Result:

text

[PASS] test_ConfigurePassAllowsLoweringMaxSupplyBelowMinted() (gas: 575637)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.49ms (529.20µs CPU time)

The invariant actual circulating supply ≤ maxSupply is permanently broken.

Recommended Mitigation

Add a strict safety check before updating maxSupply:

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");
// CRITICAL FIX: Prevent lowering maxSupply below already minted amount
+ require(maxSupply >= passSupply[passId],"Cannot set maxSupply below currently minted supply");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
passSupply[passId] = 0;
}

This change:

  • Prevents the invariant violation

  • Allows price changes and maxSupply increases safely

  • Has negligible gas overhead

  • Aligns with standard practices in capped token systems

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!