Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

getUserMemorabiliaDetailed nests an unbounded double loop over all collections and items, risking gas-exhaustion as the festival grows

getUserMemorabiliaDetailed nested unbounded loops make the view uncallable as data grows

Description

getUserMemorabiliaDetailed iterates a nested double loop over every collection (cId from 1 to nextCollectionId) and every item (iId from 1 to currentItemId) twice, once to count and once to populate (FestivalPass.sol:269-275 and 284-294). The total work grows with collections multiplied by items per collection, with no pagination or bound.

for (uint256 cId = 1; cId < nextCollectionId; cId++) {
for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) {
uint256 tokenId = encodeTokenId(cId, iId);
if (balanceOf(user, tokenId) > 0) { count++; } // @> O(collections * items), run twice
}
}

Risk

Likelihood:

High over the lifetime of a successful festival. Each createMemorabiliaCollection raises nextCollectionId and each redemption raises a collection's currentItemId, so the iteration space only ever grows and the cost is unbounded by design.

Impact:

Low. This is a view function, so it cannot corrupt state or lose funds. The harm is availability and cost: once collections and editions accumulate, the doubled balanceOf calls exceed the eth_call gas cap, and the function reverts when called on-chain (for example from another contract) and becomes slow and expensive to query off-chain, degrading any UI that depends on it.

Proof of Concept

After many collections and editions, calling the view exceeds the node gas limit and reverts.

for (uint i; i < 200; i++) pass.createMemorabiliaCollection("c","ipfs://x",1e18,500,true);
// populate editions across collections...
vm.expectRevert(); // exceeds eth_call gas; nested loops run balanceOf O(N*M) twice
pass.getUserMemorabiliaDetailed(user);

Recommended Mitigation

Track per-user token ownership in a mapping, or paginate the query with start/limit parameters.

- function getUserMemorabiliaDetailed(address user) external view returns (...) {
+ function getUserMemorabiliaDetailed(address user, uint256 startCollectionId, uint256 limit)
+ external view returns (...) {
+ // iterate only [startCollectionId, startCollectionId + limit) and return a cursor
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!