Beatland Festival

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

Incorrect `maxSupply` Check in `redeemMemorabilia` Blocks Last Token Minting

Incorrect maxSupply Check in redeemMemorabilia Blocks Last Token Minting

Description

In FastFestival::redeemMemorabilia, the function checks multiple requirements before minting a new memorabilia and one of the requirement is if the maximum supply is reached.
So, when the organizer create a new Memorabilia, the currentItemIdis hardcoded to 1(usually we think of 0 as the id of the first item), so if the organizer choose maxSupplyto be 5, then the Items Ids for this specific memorabilia will be {1, 2, 3, 4, 5}.

However, after redeeming the first 4 memorabilia succefully, the last one of Id 5 wont be able to be minted because of the following requirement require(collection.currentItemId < collection.maxSupply, "Collection sold out").

collection.currentItemId = 5 and collection.MaxSupply = 5 -> failing the requirments.

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:

  • High, this issue will occur cosistently with every new Memorabilia created.

Impact:

  • If multiple Memorabilia exist under a collectionId, the final one cannot be redeemed.

  • If a unique Memorabilia is created with maxSupply = 1, it becomes completely unredeemable.

    Prevents full redemption of Memorabilia, breaking single-item mints and leaving all collections one token short.

Proof of Concept

In the POC:

  • plug this POC in the FestivalPass.t.sol and it must work

  • I tested the case when the organizer created a Memorabilia of maxSupply = 2, where user1 will be able to redeem while user2 wont be able due to the incorrect require check.

  • To demonstrate the case of unique Memorabilia, just replace 2 by 1 in maxSupply and no one will be able to redeem the unique Memorabilia.

function test_RedeemMemorabilia_FailToMintTheLastMemorabiliaInTheCollection() public {
// Setup collection
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Shirts",
"ipfs://QmShirts",
50e18,
2, // replace this with 1 and not one can redeem it
true
);
// Give users BEAT tokens
vm.startPrank(address(festivalPass));
beatToken.mint(user1, 200e18);
beatToken.mint(user2, 200e18);
vm.stopPrank();
// User1 redeems item #1
vm.prank(user1);
festivalPass.redeemMemorabilia(collectionId);
uint256 token1 = festivalPass.encodeTokenId(collectionId, 1);
assertEq(festivalPass.balanceOf(user1, token1), 1);
// User2 redeems item #2
// Fails; the users cannot redeem the last merobilia in the collection
vm.prank(user2);
festivalPass.redeemMemorabilia(collectionId);
uint256 token2 = festivalPass.encodeTokenId(collectionId, 2);
assertEq(festivalPass.balanceOf(user2, token2), 1);
}

Recommended Mitigation

Change the requirment from < to <= (or just start the currantItemIdfrom 0 instead of 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");
//@audit-issue cannot redeen the last one; always on left (high)
- 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

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Off by one error in redeemMemorabilia

Appeal created

0xnigthswatch Submitter
29 days ago
inallhonesty Lead Judge
29 days ago
0xnigthswatch Submitter
29 days ago
inallhonesty Lead Judge 27 days 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.