Beatland Festival

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

Unbounded Iteration in `getUserMemorabiliaDetailed` — View Function Exceeds Block Gas Limit

Unbounded Iteration in getUserMemorabiliaDetailed — View Function Exceeds Block Gas Limit

Scope

  • FestivalPass.sol

Description

  • The getUserMemorabiliaDetailed() function is a view function intended to return all memorabilia NFTs owned by a user, along with their collection and item IDs.

  • The function uses nested loops iterating over all collections × all items (lines 269-276, duplicated at lines 284-294). As the number of collections and items grows, the gas cost scales as O(collections × items). Since nextCollectionId and currentItemId only increase, this function will eventually exceed the block gas limit, rendering it uncallable. Any frontend or dApp relying on this function will break.

function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
) {
uint256 count = 0;
@> 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++;
}
}
}
// ... second identical nested loop to populate arrays

Risk

Likelihood: Medium

  • Gas cost grows proportionally with protocol usage. With ~100 collections of ~100 items each, the view function exceeds standard RPC gas limits. Occurs naturally over time.

Impact: Low

  • View function becomes uncallable — no state changes lost, but UX is degraded. Frontend queries fail, requiring off-chain indexing (subgraph) as a workaround.

Severity: Low

Proof of Concept

The vulnerability is confirmed through code trace analysis. The function at lines 262-297 contains two passes over all collections and items. With nextCollectionId = N and each collection having M items on average, the function performs 2 × N × M iterations, each involving a balanceOf storage read. At an estimated 2,600 gas per SLOAD, 100 collections × 100 items = 52M gas — far exceeding the 30M block gas limit.

// Code trace analysis (no PoC forge test needed for view function DoS)
Lines 267-296: getUserMemorabiliaDetailed()
Loop 1 (count): O(nextCollectionId × max(currentItemId))
Loop 2 (populate): O(nextCollectionId × max(currentItemId))
Each iteration: encodeTokenId() + balanceOf() = ~2,600 gas (SLOAD)
At 100×100: 2 × 10,000 × 2,600 = ~52M gas > 30M block limit

Recommended Mitigation

Use an off-chain indexer (subgraph) for enumeration, or maintain a per-user owned-tokens mapping to avoid unbounded iteration.

+ // Maintain an explicit set of owned memorabilia per user
+ mapping(address => uint256[]) private userMemorabiliaTokens;
+
function redeemMemorabilia(uint256 collectionId) external {
// ... existing redemption logic ...
_mint(msg.sender, tokenId, 1, "");
+ userMemorabiliaTokens[msg.sender].push(tokenId);
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
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!