Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

[M-2] Off-by-one logic error in `FestivalPass::redeemMemorabilia` prevents minting of the final NFT in a collection

[M-2] Off-by-one logic error in `FestivalPass::redeemMemorabilia` prevents minting of the final NFT in a collection

Description

  • The `FestivalPass::redeemMemorabilia` function is used to buy a souvenir NFT. It burns BEAT tokens from the user and mints them a unique NFT from a collection of their choosing.

  • However, the function requirement checking whether the collection is sold out or not makes it so this function can only mint one NFT less than the collection's max supply, due to the `currentItemId` field starting from 1.

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");
// Burn BEAT tokens
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Generate unique token ID
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
// Store edition number
tokenIdToEdition[tokenId] = itemId;
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}

Risk

Likelihood:

  • When a user wants to buy the final NFT in a collection

Impact:

  • User can't buy the last NFT of a collection, denying them of a souvenir

Proof of Concept

1. Organizer creates a performance

2. Organizer creates a collection

3. User buys a pass

4. It's show time! User attends performance

5. User wants to buy a souvenir but is denied. The function reverts.

function testCantRedeemNFT() public {
// Organizer creates a performance
vm.prank(organizer);
uint256 startTime = block.timestamp + 1 hours;
uint256 perfId = festivalPass.createPerformance(startTime, 1 hours, 1000e18);
// Organizer creates a collection
vm.prank(organizer);
uint256 collectionMaxSupply = 1;
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Golden Hats", "ipfs://QmGoldenHats", 500e18, collectionMaxSupply, true
);
// User buys a pass
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// It's show time!
vm.warp(startTime + 30 minutes);
// User attends performance
vm.prank(user1);
festivalPass.attendPerformance(perfId);
// User wants to buy a souvenir
vm.prank(user1);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
}

Recommended Mitigation

Change the function requirement from `collection.currentItemId < collection.maxSupply` to `collection.currentItemId <= collection.maxSupply`.

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, "Collection sold out");
// Burn BEAT tokens
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Generate unique token ID
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
// Store edition number
tokenIdToEdition[tokenId] = itemId;
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 22 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!