Beatland Festival

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

getUserMemorabiliaDetailed Unbounded Nested Loop Causes Permanent Gas DoS

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

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!