Beatland Festival

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

Cannot Redeem Maximum Supply of Memorabilia NFTs

Description

The FestivalPass::redeemMemorabilia function is intended to allow users to redeem a fixed-supply NFT from a memorabilia collection. However, the current logic prevents users from redeeming the final (i.e., maxSupply-th) NFT due to an off-by-one error in this condition:

// Redeem a memorabilia NFT from a collection
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
@> require(collection.currentItemId < collection.maxSupply, "Collection sold out"); // VULNERABILITY

This check disallows redemption when currentItemId == maxSupply - 1 (just before the last mint) and then prevents minting when currentItemId == maxSupply, thereby allowing only maxSupply - 1 NFTs to be minted.

Impact:

  • The collection's maxSupply is never fully reachable.

  • It results in an under-allocation of NFTs, which could affect the scarcity and value perception of the collection.

  • Users expecting to be able to redeem up to the defined maxSupply will face unexpected reverts.

Proof of Concept:

function testCannotRedeemMaxMemoriableNft() public {
// Setup: User buys VIP pass and gets bonus BEAT
vm.prank(user1);
festivalPass.buyPass{value: VIP_PRICE}(2); // Gets 5e18 BEAT bonus
// Create performance and let user attend to earn BEAT
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(block.timestamp + 1 hours, 2 hours, 250e18);
vm.warp(block.timestamp + 90 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perfId); // Earns 500e18 BEAT
// Organizer creates a memorabilia collection with maxSupply = 5
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Festival Poster",
"ipfs://QmPosters",
100e18,
5,
true
);
// User redeems 4 out of 5 allowed NFTs successfully
vm.startPrank(user1);
festivalPass.redeemMemorabilia(collectionId); // #1
festivalPass.redeemMemorabilia(collectionId); // #2
festivalPass.redeemMemorabilia(collectionId); // #3
festivalPass.redeemMemorabilia(collectionId); // #4
// Attempt to redeem the 5th (final) NFT reverts
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId); // #5 (should succeed, but fails)
}

Root Cause:

The condition collection.currentItemId < collection.maxSupply prevents the minting of the final item. When currentItemId == maxSupply - 1, the redemption succeeds and increments the ID to maxSupply, at which point further redemptions are blocked—even though the total supply is now exactly equal to the allowed maximum.

Recommended Mitigation

Update the condition to be less than and equal to the collection.maxSupply

- require(collection.currentItemId < collection.maxSupply, "Collection sold out");
+ require(collection.currentItemId <= collection.maxSupply, "Collection sold out");

Or Alternatively we can start the currentItemId index at 0 in the FestivalPass::createMemorabiliaCollection function

collections[collectionId] = MemorabiliaCollection({
name: name,
baseUri: baseUri,
priceInBeat: priceInBeat,
maxSupply: maxSupply,
- currentItemId: 1, // Start item IDs at 1
+ currentItemId: 1, // Start item IDs at 0
isActive: activateNow
});

that way we can always mint up to the maxSupply of the NFT collection.

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Off by one error in redeemMemorabilia

Support

FAQs

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