Root + Impact
Description
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) {
@>
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");
@>
return (
collectionId,
itemId,
collection.name,
itemId,
collection.maxSupply,
uri(tokenId)
);
}
Risk
Likelihood:
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 {
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Detail Test",
"ipfs://QmDetail",
100e18,
5,
true
);
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:
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)
);
}