Beatland Festival

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

[H-2] Incorrect state reset in configurePass allows bypassing maxSupply and causes accounting desynchronization

[H-2] Incorrect state reset in configurePass allows bypassing maxSupply and causes accounting desynchronization

Description

  • The `FestivalPass::configurePass` function contains a critical logic flaw where it resets the `passSupply` to zero whenever a pass configuration is updated. This allows an organizer to inadvertently or maliciously override the global supply tracking.

  • Additionally, the function lacks a check to ensure the newMaxSupply is not lower than the current passSupply. Because the current supply is zeroed out, the contract loses track of previously minted tokens. This enables the total number of circulating passes to exceed the intended maxSupply, breaking the scarcity model of the festival tiers and leading to potential overbooking or dilution of value.

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:

  • When the organizer misconfigures a pass

Impact:

  • Over-minting: An unlimited number of passes can be minted by repeatedly calling configurePass.

  • Broken Accounting: The passSupply mapping becomes an inaccurate representation of circulating tokens, which can break downstream logic relying on supply data (e.g., secondary market royalties, rarity calculations, or access control).

Proof of Concept

The vulnerability occurs because the contract fails to preserve the historical minting state during reconfiguration.

1. Initial State: GENERAL_PASS has a maxSupply of 5000. 2000 passes are minted; `passSupply[1]` is now 2000.

2. Reconfiguration: The organizer calls `configurePass(1, 0.05 ether, 1000)`.

3. State Corruption: `passSupply[passId]` is now 0; resets the counter.

4. Violation: Users can now mint another 1000 passes. In reality, 2000 passes exist in circulation, but the contract believes only 1000 (or fewer) exist, successfully bypassing the maxSupply constraint.

function testConfigurePassCanFuckUpPassSupply() public {
// 2000 people buy GENERAL passes
uint256 numberOfPeopleWhoWantToBuyAGeneralPass = 2000;
for (uint160 i = 1; i <= numberOfPeopleWhoWantToBuyAGeneralPass; i++) {
address people = address(i);
vm.deal(people, 1 ether);
vm.prank(people);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
}
// Organizer configures the GENERAL pass supply to 1000
uint256 newMaxSupply = 1000;
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, newMaxSupply);
// The pass supply has been reset, allowing people to buy more passes
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
uint256 generalPassSupply = festivalPass.passSupply(1);
assertEq(generalPassSupply, 1);
// But 2000 people already bought it before
uint256 numberOfPeopleWhoHaveAPass = generalPassSupply;
for (uint160 i = 1; i <= numberOfPeopleWhoWantToBuyAGeneralPass; i++) {
if (festivalPass.balanceOf(address(i), 1) > 0) {
numberOfPeopleWhoHaveAPass++;
}
}
// People have more passes than the max supply
assert(numberOfPeopleWhoHaveAPass > newMaxSupply);
// And the pass supply is fucked up
assert(numberOfPeopleWhoHaveAPass > generalPassSupply);
}

Recommended Mitigation

Remove the line that resets `passSupply[passId]` to zero. Additionally, add a requirement to ensure the new maxSupply is at least equal to the number of tokens already minted to prevent state inconsistency.

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], "Max supply must be greater then current supply");
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
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!