Beatland Festival

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

[M-01] Gas-Exhaustive Enumeration — getUserMemorabiliaDetailed DoS

Gas-Exhaustive Enumeration — getUserMemorabiliaDetailed DoS

Description

  • Normal behaviour: getUserMemorabiliaDetailed(user) should return arrays of all memorabilia token IDs owned by a user.

  • Issue: The function iterates over every item ever minted across all collections, using nested for loops and calling balanceOf on each candidate ID. As collections and items grow, the loop count increases linearly until the call exceeds the block gas limit or RPC timeouts, rendering the function unusable.

// FestivalPass.sol – getUserMemorabiliaDetailed()
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++;
}
}
}

Risk

Likelihood:

  • Each new memorabilia drop increases iteration count; gas grows continuously during normal festival operations.

  • Eventually the function will hit block gas limits even for a single owner query.

Impact:

  • Denial of Service: on-chain contracts can no longer call the function.

  • Off-chain callers (front-ends) experience RPC timeouts; users cannot view their collections.

Proof of Concept

A full on-chain reproduction would require millions of items, so the PoC is expressed conceptually:

  1. Assume the festival has issued 5 000 collections with 500 memorabilia items each (reasonable after several years). That equals 2.5 million loop iterations.

  2. Any contract or user calls getUserMemorabiliaDetailed(user).

  3. The EVM must iterate through 2.5 million balanceOf checks; gas usage exceeds the block gas limit and the transaction reverts, denying service.

// Pseudo-PoC
vm.prank(user);
festivalPass.getUserMemorabiliaDetailed(user); // out-of-gas

Recommended Mitigation

Maintain a lightweight index that is updated as tokens move, making enumeration O(ownedTokens) instead of O(totalTokens). Example pattern:

- // brute-force enumeration (gas grows unbounded)
+ // mapping updated in _afterTokenTransfer
+ mapping(address => uint256[]) private ownedTokens;
function _afterTokenTransfer(
address, address from, address to,
uint256[] memory ids, uint256[] memory amounts, bytes memory
) internal override {
// remove IDs from `from`, append to `to` – keeps arrays small
}
function getUserMemorabiliaDetailed(address user) external view returns (...) {
uint256[] storage list = ownedTokens[user];
// Build and return parallel arrays here
}

This design bounds gas to the number of items actually owned by the caller—typically a few dozen—eliminating the DoS vector.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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