Description
getUserMemorabiliaDetailed is a view function that returns all memorabilia tokens owned by an address, intended to provide a complete inventory for front-end
UIs or on-chain consumers.
The function contains two nested unbounded loops: the outer loop iterates over every collection ID from 1 to nextCollectionId, and the inner loop iterates over
every item ID from 1 to currentItemId for each collection. Both bounds grow monotonically and never shrink. For each combination it calls balanceOf, a storage
read. As the protocol scales, this function exceeds node RPC gas limits (typically 50M gas) and becomes permanently uncallable.
// src/FestivalPass.sol
function getUserMemorabiliaDetailed(address user) external view returns (...) {
uint256 count = 0;
@> for (uint256 cId = 1; cId < nextCollectionId; cId++) { // O(collections)
@> for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) { // O(items)
uint256 tokenId = encodeTokenId(cId, iId);
@> if (balanceOf(user, tokenId) > 0) { // cold SLOAD ~2100 gas per iteration
count++;
}
}
}
// Second identical nested loop to populate arrays — doubles the cost
// Total: O(2 × collections × items) storage reads
}
Risk
Likelihood:
The function becomes unusable once total (collections × avg items) exceeds ~25,000, reached with 100 collections averaging 250 items each — a realistic
production scenario for a live festival.
nextCollectionId starts at 100 and never decreases, meaning even empty collection slots (IDs 1–99) are iterated on every call from day one, wasting gas before
any real data exists.
Impact:
Any on-chain contract that calls this view function to enumerate user holdings will have all its transactions revert out-of-gas once the threshold is crossed,
with no upgrade path.
Front-end UIs relying on this function via eth_call will silently fail for all users as the collection space grows.
Proof of Concept
function test_GetUserMemorabiliaDetailedDoS() public {
// Create 200 collections — realistic for a multi-day festival
vm.startPrank(organizer);
for (uint256 i = 0; i 0) { count++; }
}
}
// ... second identical loop
}
// Option A — paginated on-chain query:
function getUserMemorabiliaDetailedPaginated(
address user,
uint256 startCollectionId,
uint256 endCollectionId
) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
) {
require(endCollectionId <= nextCollectionId, "Out of range");
require(endCollectionId - startCollectionId <= 20, "Page too large");
// iterate only [startCollectionId, endCollectionId)
}
// Option B — index off-chain via MemorabiliaRedeemed events (already emitted):
// event MemorabiliaRedeemed(address indexed collector, uint256 indexed tokenId, ...)
// Build user inventory from event logs — no on-chain loop needed
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.