The uri() function in FestivalPass.sol is designed to return metadata URLs for minted tokens, and under normal operation, the function should only return valid URIs for tokens that actually exist, reverting or returning empty strings for non-existent tokens to maintain consistency with standard ERC1155 behavior.
The function validates collection existence but not whether specific items within that collection have been minted. A collection with only 3 minted items (currentItemId = 4) will return valid metadata URIs for item IDs 50, 99, or any number, creating inconsistency where balanceOf() returns 0 but uri() returns a valid string.
Likelihood:
The vulnerability manifests when any external system queries metadata for token IDs that haven't been minted yet, occurring naturally as marketplaces scan token ID ranges or users manually check future item numbers.
Standard NFT tooling often assumes uri() availability indicates token existence, making misinterpretation likely without additional existence checks.
Impact:
User confusion arises when marketplace displays show items that cannot be transferred or owned, and developers integrating with the protocol encounter inconsistency between existence queries (balanceOf) and metadata queries (uri).
Protocol appears unprofessional when non-existent tokens return metadata, violating common ERC1155 expectations and patterns.
Add existence validation to ensure only minted tokens return metadata URIs.
### 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: ```solidity 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: ```solidity 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: ```diff 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.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.