Beatland Festival

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

Off by one error in Memorabilia redemption cap

Incorrect boundary condition leads to under minting

Description

  • The FestivalPass::redeemMemorabilia function is supposed to allow minting of the memorabilia NFT from a collection up to the maximum supply.

  • The FestivalPass::redeemMemorabilia function enforces a cap on the number of items minted from a collection using the condition:

require(collection.currentItemId < collection.maxSupply, "Collection sold out");

This logic causes the function to disallow minting the last item

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:

  • This happens when trying to mint the last item in the collection

Impact:

  • Each memorabilia collection will have one fewer item minted than intended

Proof of Concept

Add this test to the FestivalPass.t.sol

  • Organizer creates a collection with a maximum supply of 5

  • The first four users claim the collection each

  • But the fifth user fails to throwing the Collection sold out error

function test_cant_mint_up_to_cap() public {
// Organizer creates a collection with max supply of 5
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection("Poster", "ipfs://poster", 100e18, 5, true);
// Create addresses
address user1 = makeAddr("1");
address user2 = makeAddr("2");
address user3 = makeAddr("3");
address user4 = makeAddr("4");
address user5 = makeAddr("5");
// Mint BEAT tokens to 5 users
vm.startPrank(address(festivalPass));
beatToken.mint(user1, 200e18);
beatToken.mint(user2, 200e18);
beatToken.mint(user3, 200e18);
beatToken.mint(user4, 200e18);
beatToken.mint(user5, 200e18);
vm.stopPrank();
// Users redeem
address[5] memory users = [user1, user2, user3, user4, user5];
for (uint256 i = 0; i < 5; i++) {
vm.prank(users[i]);
festivalPass.redeemMemorabilia(collectionId);
}
}

Recommended Mitigation

Change the conditional check to <= instead of <

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

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.