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.
Impact:
NFT marketplaces might display non-existent items as available
Confusion about which items in a collection actually exist.
The function should also verify that itemId is within the range of actually minted items (itemId > 0 && itemId < collections[collectionId].currentItemId).
### 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.