Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Unbounded Loops Cause Permanent DoS in Memorabilia Query Function

Root + Impact

Description

The normal behavior should be that getUserMemorabiliaDetailed() returns a user's memorabilia tokens efficiently regardless of the total number of collections and items in the system.

The specific issue is that the function uses nested unbounded loops that iterate through all collections and all items within each collection, creating O(n²) complexity that will exceed gas limits as the system scales.

// Root cause in the codebase with @> marks to highlight the relevant section
function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
) {
// First, count how many memorabilia they own
uint256 count = 0;
for (uint256 cId = 1; cId < nextCollectionId; cId++) { // @> Unbounded outer loop
for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) { // @> Unbounded inner loop
uint256 tokenId = encodeTokenId(cId, iId);
if (balanceOf(user, tokenId) > 0) {
count++;
}
}
}
// Then populate arrays
tokenIds = new uint256[](count);
collectionIds = new uint256[](count);
itemIds = new uint256[](count);
uint256 index = 0;
for (uint256 cId = 1; cId < nextCollectionId; cId++) { // @> Second unbounded outer loop
for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) { // @> Second unbounded inner loop
uint256 tokenId = encodeTokenId(cId, iId);
if (balanceOf(user, tokenId) > 0) {
tokenIds[index] = tokenId;
collectionIds[index] = cId;
itemIds[index] = iId;
index++;
}
}
}
}

Risk

Likelihood:

  • Function will be called frequently by frontends to display user collections

  • System is designed to support multiple collections with many items each

  • Gas consumption grows quadratically with adoption

Impact:

  • Function becomes permanently unusable once sufficient collections exist

  • Frontend integration breaks, preventing users from viewing their assets

  • No workaround exists without contract upgrade



Proof of Concept

// Scenario: Moderate usage after 6 months
// - 50 memorabilia collections created
// - Average 100 items per collection = 5,000 total items
// - Function performs: 50 * 100 * 2 = 10,000 iterations
// - Each iteration includes balanceOf() call (expensive storage read)
// - Total gas: ~10,000 * 2,100 = 21M gas (exceeds block limit)
// Function call will revert with "out of gas" error
getUserMemorabiliaDetailed(userAddress); // REVERTS

Recommended Mitigation

- function getUserMemorabiliaDetailed(address user) external view returns (...)
+ function getUserMemorabiliaDetailed(
+ address user,
+ uint256 startCollection,
+ uint256 maxCollections
+ ) external view returns (...)
{
uint256 count = 0;
- for (uint256 cId = 1; cId < nextCollectionId; cId++) {
+ uint256 endCollection = startCollection + maxCollections;
+ if (endCollection > nextCollectionId) endCollection = nextCollectionId;
+
+ for (uint256 cId = startCollection; cId < endCollection; cId++) {
// ... rest of function with pagination
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 28 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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