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 currentItemId
is hardcoded to 1
(usually we think of 0 as the id of the first item), so if the organizer choose maxSupply
to 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");
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
tokenIdToEdition[tokenId] = itemId;
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
Risk
Likelihood:
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 {
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Shirts",
"ipfs://QmShirts",
50e18,
2,
true
);
vm.startPrank(address(festivalPass));
beatToken.mint(user1, 200e18);
beatToken.mint(user2, 200e18);
vm.stopPrank();
vm.prank(user1);
festivalPass.redeemMemorabilia(collectionId);
uint256 token1 = festivalPass.encodeTokenId(collectionId, 1);
assertEq(festivalPass.balanceOf(user1, token1), 1);
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 currantItemId
from 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);
}