Beatland Festival

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

FestivalPass.sol - URI Function Returns Metadata for Non-Existent Items

Description

The uri function returns metadata URLs for any token ID that belongs to an existing collection, even if the specific item within that collection was never minted. This creates confusion about which tokens actually exist and can cause integration issues with external systems that rely on URI responses to determine token validity.

Root Cause

The URI function only validates that the collection exists but doesn't verify that the specific item was actually minted:

function uri(uint256 tokenId) public view override returns (string memory) {
// Handle regular passes (IDs 1-3)
if (tokenId <= BACKSTAGE_PASS) {
return string(abi.encodePacked("ipfs://beatdrop/", Strings.toString(tokenId)));
}
// Decode collection and item IDs
(uint256 collectionId, uint256 itemId) = decodeTokenId(tokenId);
// Check if it's a valid memorabilia token
if (collections[collectionId].priceInBeat > 0) {
// ❌ Returns URI even for non-existent items!
return string(abi.encodePacked(
collections[collectionId].baseUri,
"/metadata/",
Strings.toString(itemId)
));
}
return super.uri(tokenId);
}

The function should also verify that itemId is within the range of actually minted items (itemId > 0 && itemId < collections[collectionId].currentItemId).

Risk

Likelihood: Medium - Any external system querying URIs for memorabilia tokens can encounter this issue when checking non-existent item IDs.

Impact: Low - No funds are at risk, but metadata integrity is compromised and external integrations may be confused.

Impact

  • External systems receive metadata URLs for tokens that were never minted

  • NFT marketplaces might display non-existent items as available

  • Inconsistent behavior between balanceOf() (returns 0 for non-existent tokens) and uri() (returns metadata)

  • Confusion about which items in a collection actually exist

  • Potential integration failures with systems expecting URI calls to fail for non-existent tokens

Proof of Concept

This test demonstrates how the URI function returns metadata for items that were never minted:

function test_URIReturnsInvalidMetadataForNonExistentItems() public {
// Organizer creates a collection with maxSupply = 5
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Test Collection",
"ipfs://testbase",
50e18,
5, // maxSupply = 5
true
);
// Give user BEAT tokens and let them redeem 2 items
vm.prank(address(festivalPass));
beatToken.mint(user1, 200e18);
// User redeems 2 items (itemIds 1 and 2)
vm.startPrank(user1);
festivalPass.redeemMemorabilia(collectionId); // Item 1
festivalPass.redeemMemorabilia(collectionId); // Item 2
vm.stopPrank();
// Collection now has currentItemId = 3 (next item to be minted)
// Only items 1 and 2 actually exist
// Encode token IDs for existing and non-existing items
uint256 existingItem1 = festivalPass.encodeTokenId(collectionId, 1);
uint256 existingItem2 = festivalPass.encodeTokenId(collectionId, 2);
uint256 nonExistentItem3 = festivalPass.encodeTokenId(collectionId, 3);
uint256 nonExistentItem6 = festivalPass.encodeTokenId(collectionId, 6);
// Verify only items 1 and 2 actually exist (user owns them)
assertEq(festivalPass.balanceOf(user1, existingItem1), 1);
assertEq(festivalPass.balanceOf(user1, existingItem2), 1);
assertEq(festivalPass.balanceOf(user1, nonExistentItem3), 0);
assertEq(festivalPass.balanceOf(user1, nonExistentItem6), 0);
// BUT uri() function returns metadata URLs for ALL items, even non-existent ones!
string memory uri1 = festivalPass.uri(existingItem1);
string memory uri2 = festivalPass.uri(existingItem2);
string memory uri3 = festivalPass.uri(nonExistentItem3); // Should not exist!
string memory uri6 = festivalPass.uri(nonExistentItem6); // Should not exist!
// All URIs are returned even for non-existent items
assertEq(uri1, "ipfs://testbase/metadata/1");
assertEq(uri2, "ipfs://testbase/metadata/2");
assertEq(uri3, "ipfs://testbase/metadata/3"); // ❌ This shouldn't exist
assertEq(uri6, "ipfs://testbase/metadata/6"); // ❌ This shouldn't exist
// This creates confusion - external systems get metadata URLs for tokens that were never minted
console.log("URI for non-existent item 3:", uri3);
console.log("URI for non-existent item 6:", uri6);
}

Recommended Mitigation

Add validation to ensure the requested item actually exists within the collection:

function uri(uint256 tokenId) public view override returns (string memory) {
// Handle regular passes (IDs 1-3)
if (tokenId <= BACKSTAGE_PASS) {
return string(abi.encodePacked("ipfs://beatdrop/", Strings.toString(tokenId)));
}
// Decode collection and item IDs
(uint256 collectionId, uint256 itemId) = decodeTokenId(tokenId);
// Check if it's a valid memorabilia token
if (collections[collectionId].priceInBeat > 0) {
+ // Validate that the item actually exists
+ require(itemId > 0 && itemId < collections[collectionId].currentItemId, "Item does not exist");
return string(abi.encodePacked(
collections[collectionId].baseUri,
"/metadata/",
Strings.toString(itemId)
));
}
return super.uri(tokenId);
}

This ensures that URI calls will fail for non-existent items, providing consistent behavior with the rest of the contract and preventing confusion for external integrators.

Updates

Lead Judging Commences

inallhonesty Lead Judge 30 days ago
Submission Judgement Published
Validated
Assigned finding tags:

uri for non-existing ids

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.