Beatland Festival

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

Incorrect calculation in checking if the Collection is sold out in `FestivalPass::redeemMemorabilia`

Root + Impact

Description

The collection.currentItemId Start at 1,but this was not taken into account when calculating the total amount.

// Create a new memorabilia collection
function createMemorabiliaCollection(
string memory name,
string memory baseUri,
uint256 priceInBeat,
uint256 maxSupply,
bool activateNow
) external onlyOrganizer returns (uint256) {
require(priceInBeat > 0, "Price must be greater than 0");
require(maxSupply > 0, "Supply must be at least 1");
require(bytes(name).length > 0, "Name required");
require(bytes(baseUri).length > 0, "URI required");
uint256 collectionId = nextCollectionId++;
collections[collectionId] = MemorabiliaCollection({
name: name,
baseUri: baseUri,
priceInBeat: priceInBeat,
maxSupply: maxSupply,
currentItemId: 1, // Start item IDs at 1 //@> NOTE this
isActive: activateNow
});
emit CollectionCreated(collectionId, name, maxSupply);
return collectionId;
}

In the following code, collection.currentItemId is assumed to start counting from 0.

// Redeem a memorabilia NFT from a collection
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
require(collection.currentItemId < collection.maxSupply, "Collection sold out");//@> FIXME:if maxSupply ==1, revert

Risk

Likelihood: High

This feature is widely used.

Impact: Medium

The total amount that can be redeemed is one less than expected.

Proof of Concept

function test_RedeemMemorabilia_Only_One() public {
// Setup: User needs BEAT tokens
vm.prank(user1);
festivalPass.buyPass{value: VIP_PRICE}(2); // Gets 5e18 BEAT bonus
// Create performance and earn more 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 (250e18 * 2x VIP multiplier)
// Create memorabilia collection
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Festival Poster",
"ipfs://QmPosters",
100e18,
1,//>@ Audit: only one
true
);
// expect Revert
vm.prank(user1);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
}

Recommended Mitigation

// Redeem a memorabilia NFT from a collection
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
- require(collection.currentItemId < collection.maxSupply, "Collection sold out");
+ require(collection.currentItemId < collection.maxSupply+1, "Collection sold out");
Updates

Lead Judging Commences

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