Beatland Festival

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

Off-by-One Error in redeemMemorabilia() Prevents Final NFT Mint Per Collection

Root + Impact

Description

  • The redeemMemorabilia() function should allow minting NFTs from item ID 1 up to the collection's maximum supply limit, enabling all promised NFTs in a collection to be minted.

  • An off-by-one error in the supply validation logic prevents the final NFT in each collection from being minted, causing direct economic loss to the protocol.

function redeemMemorabilia(uint256 collectionId) external {
Collection storage collection = collections[collectionId];
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++;
_mint(msg.sender, collectionId, itemId, "");
emit MemorabiliaRedeemed(msg.sender, collectionId, itemId);
}

Risk

Likelihood:

  • Every collection will lose exactly one NFT mint when the currentItemId reaches the maxSupply value

  • Users attempting to mint the final item in any collection will encounter a "Collection sold out" error despite available supply

Impact:

  • Direct revenue loss of 1 NFT worth of BEAT tokens per collection (50 BEAT tokens per collection)

  • User confusion and support burden when collections appear sold out prematurely

  • Potential regulatory issues from undelivered digital assets advertised as available

Proof of Concept

This test simulates the minting process for a newly created collection with a maxSupply of 3. We expect the collection to allow three NFTs to be minted successfully.

The test uses three separate user addresses to redeem memorabilia one by one:

  • The first and second users can successfully mint items with currentItemId values of 1 and 2, respectively.

  • However, the third user fails to mint, even though the maximum supply hasn’t been reached.

This is due to the condition in the redeemMemorabilia() function:

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

Here, currentItemId starts at 1. So when the third mint is attempted, currentItemId equals maxSupply (3), causing the condition to fail (3 < 3 is false).

As a result, only 2 items can ever be minted for a collection with a maxSupply of 3 — one less than intended. This clearly shows the off-by-one error, which prevents the full utilization of the collection's configured supply.

The final assertEq confirms this by checking that only 2 NFTs were actually minted.

function testOffByOneExploit() public {
// Create collection: maxSupply = 3, expecting items 1,2,3
uint256 collectionId = festivalPass.createCollection("Test", 3, 10e18);
address[3] memory users = [address(0x1), address(0x2), address(0x3)];
for(uint i = 0; i < 3; i++) {
beatToken.mint(users[i], 10e18);
}
// Mint #1: currentItemId=1, 1 < 3
vm.prank(users[0]);
festivalPass.redeemMemorabilia(collectionId);
// Mint #2: currentItemId=2, 2 < 3
vm.prank(users[1]);
festivalPass.redeemMemorabilia(collectionId);
// Mint #3: currentItemId=3, 3 < 3 FAILS!
vm.prank(users[2]);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
// Verification: Only 2 NFTs minted instead of promised 3
assertEq(festivalPass.totalSupply(collectionId), 2);
}

Recommended Mitigation

Change the boundary condition to use <= instead of <. This allows currentItemId values from 1 through maxSupply inclusive, enabling all promised NFTs to be minted:

require(collection.currentItemId <= collection.maxSupply, "Collection sold out");
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

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