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 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Off by one error in redeemMemorabilia

Appeal created

0xnigthswatch Submitter
3 months ago
inallhonesty Lead Judge
3 months ago
0xnigthswatch Submitter
3 months ago
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.