Beatland Festival

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

Off-by-One Error Prevents Minting the Last Memorabilia Item

Root + Impact

Descriptiom

  • Normal : The redeemMemorabilia function is expected to allow users to mint memorabilia NFTs one by one until the collection's defined maxSupply is reached.

  • Issue : An off-by-one error in the supply check logic causes the sale to end one item too early, preventing the final item of every collection from ever being minted.

// Root cause in the codebase with @> marks to highlight the relevant section
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 bug is guaranteed to occur for every memorabilia collection created by the organizer.


Impact: Medium

  • Broken Core Logic: The contract fails to fulfill its promise of selling maxSupply items, as the true available supply is always maxSupply - 1. This leads to user dissatisfaction when the final advertised item is unobtainable.

  • Permanently Locked Asset: The final NFT of every collection is locked in the contract forever, becoming inaccessible and unmintable.

Proof of Concept

testcase to show minting last NFT causes a revert

function testExploit_OffByOne_CannotMintLastItem() public {
// 1. ARRANGE: Create a collection with a max supply of only 2.
vm.startPrank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection("Off-by-one Test", "ipfs://test", 10, 2, true);
vm.stopPrank();
// Give user1 enough BEAT tokens for both mints
vm.prank(address(festivalPass));
beatToken.mint(user1, 100);
// 2. ACT: The first mint (item #1) succeeds.
vm.prank(user1);
festivalPass.redeemMemorabilia(collectionId);
// After the first mint, currentItemId is 2 and maxSupply is 2.
// 3. ASSERT: The attempt to mint the second and final item fails.
// The check `require(2 < 2)` will revert with "Collection sold out".
vm.prank(user1);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
}

Recommended Mitigation

Change the strict less-than operator (<) to less-than-or-equal-to (<=) to allow the currentItemId to equal the maxSupply for the final mint.

- require(collection.currentItemId < collection.maxSupply, "Collection sold out");
+ require(collection.currentItemId <= collection.maxSupply, "Collection sold out");
Updates

Lead Judging Commences

inallhonesty Lead Judge 29 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.