Beatland Festival

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

Missing Bounds Check on itemId in `uri()` and `getMemorabiliaDetails()`

Root + Impact

Description

  • After decoding tokenId into collectionId and itemId, both uri() and getMemorabiliaDetails() lack the check if itemId excesses maximum supply

function uri(uint256 tokenId) public view override returns (string memory) {
if (tokenId <= BACKSTAGE_PASS) {
return string(abi.encodePacked("ipfs://beatdrop/", Strings.toString(tokenId)));
}
(uint256 collectionId, uint256 itemId) = decodeTokenId(tokenId);
if (collections[collectionId].priceInBeat > 0) {
@> // lack the check if itemId > collections[collectionId].maxSupply
return string(abi.encodePacked(
collections[collectionId].baseUri,
"/metadata/",
Strings.toString(itemId)
));
}
return super.uri(tokenId);
}
function getMemorabiliaDetails(uint256 tokenId) external view returns (
uint256 collectionId,
uint256 itemId,
string memory collectionName,
uint256 editionNumber,
uint256 maxSupply,
string memory tokenUri
) {
(collectionId, itemId) = decodeTokenId(tokenId);
MemorabiliaCollection memory collection = collections[collectionId];
require(collection.priceInBeat > 0, "Invalid token");
@> // lack the check if itemId > collection.maxSupply
return (
collectionId,
itemId,
collection.name,
itemId,
collection.maxSupply,
uri(tokenId)
);
}

Risk

Likelihood:

  • Anyone can simply call these functions with non-exist tokenId to get the fake uri and Memorabilia Detail

Impact:

  • User confusion or UI inconsistencies (e.g., ghost items showing up in wallets or collections)

  • Potential abuse in off-chain indexing (e.g., platforms misrepresenting available items)

Proof of Concept

Add the following test and run the command: forge test -vv --match-test test_GetMemorabiliaDetails_InvalidItemId

function test_GetMemorabiliaDetails_InvalidItemId() public {
// Setup
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Detail Test",
"ipfs://QmDetail",
100e18,
5,
true
);
// Get details for item #6 although maxSupply == 5
uint256 tokenId = festivalPass.encodeTokenId(collectionId, 6);
(
uint256 retColId,
uint256 retItemId,
string memory colName,
uint256 edition,
uint256 maxSupply,
string memory tokenUri
) = festivalPass.getMemorabiliaDetails(tokenId);
console.log("Details for item #6 is retrieved, despite max supply being 5:\n");
console.log("Collection ID: ", retColId);
console.log("Item ID: ", retItemId);
console.log("Collection Name: ", colName);
console.log("Edition: ", edition);
console.log("Max Supply: ", maxSupply);
console.log("Token URI: ", tokenUri);
}

PoC Results:

forge test -vv --match-test test_GetMemorabiliaDetails_InvalidItemId
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.25
[⠢] Solc 0.8.25 finished in 1.07s
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_GetMemorabiliaDetails_InvalidItemId() (gas: 178950)
Logs:
Details for item #6 is retrieved, despite max supply being 5:
Collection ID: 100
Item ID: 6
Collection Name: Detail Test
Edition: 6
Max Supply: 5
Token URI: ipfs://QmDetail/metadata/6
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.20ms (1.70ms CPU time)
Ran 1 test suite in 329.01ms (8.20ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Add itemId check on both uri() and getMemorabiliaDetails()

function uri(uint256 tokenId) public view override returns (string memory) {
if (tokenId <= BACKSTAGE_PASS) {
return string(abi.encodePacked("ipfs://beatdrop/", Strings.toString(tokenId)));
}
(uint256 collectionId, uint256 itemId) = decodeTokenId(tokenId);
if (collections[collectionId].priceInBeat > 0) {
+ if (collections[collectionId].maxSupply < itemId) return super.uri(tokenId);
return string(abi.encodePacked(
collections[collectionId].baseUri,
"/metadata/",
Strings.toString(itemId)
));
}
return super.uri(tokenId);
}
function getMemorabiliaDetails(uint256 tokenId) external view returns (
uint256 collectionId,
uint256 itemId,
string memory collectionName,
uint256 editionNumber,
uint256 maxSupply,
string memory tokenUri
) {
(collectionId, itemId) = decodeTokenId(tokenId);
MemorabiliaCollection memory collection = collections[collectionId];
require(collection.priceInBeat > 0, "Invalid token");
+ require(collection.priceInBeat >= itemId, "Invalid token");
return (
collectionId,
itemId,
collection.name,
itemId, // Edition number is the item ID
collection.maxSupply,
uri(tokenId)
);
}
Updates

Lead Judging Commences

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

Give us feedback!