Beatland Festival

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

Re-running `configurePass` Breaks Supply Invariants by Overwriting `maxSupply` and Resetting `passSupply`

Re-running configurePass Breaks Supply Invariants by Overwriting maxSupply and Resetting passSupply

Description

  • The configurePass function allows a trusted organizer to set the price and maximum supply cap for each pass type.

  • When configurePass is called on an already-configured pass, it both overwrites the existing passMaxSupply and resets the passSupply counter to zero. This combination breaks the supply accounting, as the contract forgets both the original supply limit and the number of passes that have already been minted and sold.

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:

  • The organizer calls configurePass for a passId that has already been configured and has had passes sold.

Impact:

  • This accounting break allows the total number of minted passes to exceed the intended maxSupply limit, breaking a core protocol invariant. For example, if 50 passes are sold from an initial maxSupply of 100, and the organizer then re-runs configurePass with a new maxSupply of 100, the old limit is forgotten and the counter is reset. This would allow another 100 passes to be sold for a total of 150.

Proof of Concept

The following test shows the organizer re-configuring the general pass after one has already been sold. This allows the same user to buy a second pass, exceeding the maxSupply of 1.

function test_Audit_ConfigurePassToExistingPass() public {
// Configure a pass with max supply of 1
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 1);
// First purchase succeeds
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Re-configure the same pass with max supply of 1
vm.prank(organizer);
festivalPass.configurePass(1, GENERAL_PRICE, 1);
// Second purchase succeeds again because the supply was reset
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
uint256 passMaxSupply = festivalPass.passMaxSupply(1);
uint256 passSupply = festivalPass.passSupply(1);
uint256 userPassBalance = festivalPass.balanceOf(user1, 1);
// User's pass count (2) now exceeds the pass max supply (1) and current supply (1)
assertLt(passMaxSupply, userPassBalance);
assertLt(passSupply, userPassBalance);
// Pass max supply: 1
// Pass current supply: 1
// User pass balance: 2
console.log("Pass max supply:", passMaxSupply);
console.log("Pass current supply:", passSupply);
console.log("User pass balance:", userPassBalance);
}

Recommended Mitigation

Add the new maxSupply to the existing passMaxSupply and preserve the current passSupply to ensure accurate 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; // Add to existing max supply
- passMaxSupply[passId] = maxSupply;
- passSupply[passId] = 0; // Reset current supply
}
Updates

Lead Judging Commences

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