Beatland Festival

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

Off-by-One error in `redeemMemorabilia` function prevents full collection redemption and unique item (`maxSupply == 1`) redemption

Off-by-One error in redeemMemorabilia function prevents full collection redemption and unique item (maxSupply == 1) redemption

Description

  • The FestivalPass contract allows users to redeem memorabilia NFTs from a collection by burning BEAT tokens, with each collection having a defined maxSupply to limit the number of items that can be minted.

  • Due to an off-by-one error in the redeemMemorabilia function, users can only redeem maxSupply - 1 items in a collection, preventing the redemption of the final item, which is critical for collections where maxSupply is 1 (i.e., a unique item).

// Root cause in FestivalPass.sol
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:

  • Occurs whenever a user attempts to redeem an item from a collection where currentItemId equals maxSupply - 1, as the require check will pass, but the next redemption will fail due to currentItemId reaching maxSupply.

  • Always occurs for collections with maxSupply set to 1, as the first redemption attempt will fail since currentItemId (starting at 1) is not less than maxSupply.

Impact:

  • Users cannot redeem the final item in a collection, reducing the total number of redeemable items to maxSupply - 1, which diminishes the collection’s intended value and utility.

  • For unique item collections (maxSupply == 1), no items can be redeemed, rendering the collection unusable.

Proof of Concept

Add the following test to test/FestivalPass.t.sol and run forge test --mt test_RedeemMemorabilia_CanNotRedeemUniqueItem -vvv.

function test_RedeemMemorabilia_CanNotRedeemUniqueItem() public {
// Setup collection with 1 item
vm.prank(organizer);
uint256 collectionId =
festivalPass.createMemorabiliaCollection("Unique Shirt", "ipfs://QmUniqueShirt", 1000e18, 1, true);
// Give user BEAT tokens
vm.startPrank(address(festivalPass));
beatToken.mint(user1, 1000e18);
vm.stopPrank();
// User tries to redeem the item
vm.prank(user1);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
console.log("===== User is unable to redeem the unique item =====");
}

Expected output:

Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_RedeemMemorabilia_CanNotRedeemUniqueItem() (gas: 223115)
Logs:
===== User is unable to redeem the unique item =====

Recommended Mitigation

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.