Beatland Festival

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

FestivalPass::getUserMemorabiliaDetailed` function has unbounded gas consumption that can cause DoS

FestivalPass::getUserMemorabiliaDetailed function has unbounded gas consumption that can cause DoS

Description

The FestivalPass::getUserMemorabiliaDetailed function uses nested loops to iterate through ALL collections and ALL items ever created, regardless of whether the user owns them. This creates O(collections × items) complexity that grows unbounded as the platform scales.

The function iterates twice through the entire collection/item space - once to count, once to populate arrays - making it even more expensive.

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++) {
@> for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) {
uint256 tokenId = encodeTokenId(cId, iId);
if (balanceOf(user, tokenId) > 0) {
count++;
}
}
}
// Then populate arrays - ITERATES AGAIN
@> for (uint256 cId = 1; cId < nextCollectionId; cId++) {
@> for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) {
// ...
}
}
}

Risk

Likelihood:

  • This will occur as the platform grows and more collections/items are created.

  • The organizer can accelerate this by creating many collections with high maxSupply values.

Impact:

  • Any smart contract calling this function will run out of gas and revert, breaking integrations.

  • RPC nodes have execution limits for eth_call - the function will timeout or fail for frontends.

  • Users cannot retrieve their memorabilia data reliably, degrading UX.

Proof of Concept

  1. Organizer creates multiple collections with items minted

  2. As collections/items grow, gas cost increases quadratically

  3. Eventually the function exceeds gas limits and becomes unusable

function testGetUserMemorabiliaDetailedDoS() public {
// Create many collections with items
for (uint256 i = 0; i < 50; i++) {
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
string(abi.encodePacked("Collection", i)),
"ipfs://test",
10e18,
100, // 100 items per collection
true
);
// Mint some items in each collection
address minter = makeAddr(string(abi.encodePacked("minter", i)));
vm.prank(address(festivalPass));
beatToken.mint(minter, 1000e18);
vm.prank(minter);
beatToken.approve(address(festivalPass), type(uint256).max);
for (uint256 j = 0; j < 10; j++) {
vm.prank(minter);
festivalPass.redeemMemorabilia(collectionId);
}
}
// Now try to get user memorabilia - expensive!
// 50 collections × 10 items × 2 loops = 1000 iterations minimum
// Gas cost grows as more collections/items are added
address user = makeAddr("user");
uint256 gasBefore = gasleft();
festivalPass.getUserMemorabiliaDetailed(user);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used:", gasUsed);
// As platform grows, this becomes prohibitively expensive
}

Recommended Mitigation

Replace the unbounded iteration with user-specific tracking. Maintain a mapping of user addresses to their owned token IDs:

Note: This mitigation requires additional logic to handle transfers (override _update to maintain the mapping when tokens change hands).

+ mapping(address => uint256[]) private userMemorabilia;
function redeemMemorabilia(uint256 collectionId) external {
// ... existing logic ...
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
+ userMemorabilia[msg.sender].push(tokenId);
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
+ function getUserMemorabiliaDetailed(address user)
+ external
+ view
+ returns (uint256[] memory tokenIds, uint256[] memory collectionIds, uint256[] memory itemIds)
+ {
+ uint256[] memory owned = userMemorabilia[user];
+ uint256 length = owned.length;
+
+ tokenIds = new uint256[](length);
+ collectionIds = new uint256[](length);
+ itemIds = new uint256[](length);
+
+ for (uint256 i = 0; i < length; i++) {
+ tokenIds[i] = owned[i];
+ (collectionIds[i], itemIds[i]) = decodeTokenId(owned[i]);
+ }
+
+ return (tokenIds, collectionIds, itemIds);
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 22 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!