Beatland Festival

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

function getUserMemorabiliaDetailed_gas_dos

Root + Impact

Description

Normal behavior

The getUserMemorabiliaDetailed function is intended to return all memorabilia NFTs owned by a given user, including their token IDs, collection IDs, and item IDs, by iterating through existing memorabilia collections and checking balances.

Specific issue

The function performs nested, unbounded loops over all existing collections and all items within each collection, regardless of whether the queried user owns those NFTs.
As the number of collections and items grows, the gas cost of this function increases linearly (or worse), eventually exceeding gas limits and causing the function to become unusable.
This allows a malicious or compromised organizer to permanently deny service to this function by inflating on-chain state.

// 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
) {
uint256 count = 0;
for (uint256 cId = 1; cId < nextCollectionId; cId++) {
for (uint256 iId = 1; iId < collections[cId].currentItemId; iId++) {
uint256 tokenId = encodeTokenId(cId, iId);
// @> Unbounded iteration over all collections and items
if (balanceOf(user, tokenId) > 0) {
count++;
}
}
}
...
}

Risk

Likelihood:

  • The organizer role can continuously create new memorabilia collections and increase currentItemId by redeeming items, which monotonically increases the iteration space.

  • The function is expected to be called by frontends, wallets, or indexers, making gas exhaustion highly likely as the protocol scales.

Impact:

  • The function will eventually exceed block gas limits or RPC execution limits, making it impossible to call.

  • Frontend applications, indexers, and user interfaces relying on this function will fail, resulting in a permanent denial of service.

Proof of Concept

function test_GasDOS_getUserMemorabiliaDetailed() public {
vm.startPrank(organizer);
uint256 COLLECTIONS = 30;
uint256 ITEMS = 30;
// 1. create many collection
for (uint256 c = 0; c < COLLECTIONS; c++) {
festivalPass.createMemorabiliaCollection(
"COLL",
"ipfs://test",
1e18,
ITEMS + 1,
true
);
}
vm.stopPrank();
// 2. give attacker enough BEAT to get too many item
vm.prank(address(festivalPass));
beatToken.mint(organizer, 1_000_000e18);
// 3. organizer 把每个 collection 都 redeem 到 max
vm.startPrank(organizer);
for (uint256 cId = 100; cId < 100 + COLLECTIONS; cId++) {
for (uint256 i = 0; i < ITEMS; i++) {
festivalPass.redeemMemorabilia(cId);
}
}
vm.stopPrank();
// 4. test gas
uint256 gasBefore = gasleft();
festivalPass.getUserMemorabiliaDetailed(user1);
uint256 gasUsed = gasBefore - gasleft();
emit log_named_uint("Gas used", gasUsed);
//
assertGt(gasUsed, 3_000_000);
}

The function will eventually exceed block gas limits or RPC execution limits, making it impossible to call.

Frontend applications, indexers, and user interfaces relying on this function will fail, resulting in a permanent denial of service.

Recommended Mitigation

Avoid iterating over global state.
Instead, maintain per-user memorabilia ownership indexes updated during mint and burn operations.

+ mapping(address => uint256[]) private userMemorabilia;
+ mapping(address => mapping(uint256 => bool)) private owned;
function redeemMemorabilia(uint256 collectionId) external {
...
uint256 tokenId = encodeTokenId(collectionId, itemId);
+ if (!owned[msg.sender][tokenId]) {
+ owned[msg.sender][tokenId] = true;
+ userMemorabilia[msg.sender].push(tokenId);
+ }
_mint(msg.sender, tokenId, 1, "");
}
- function getUserMemorabiliaDetailed(address user) external view returns (...)
+ function getUserMemorabilia(address user)
+ external
+ view
+ returns (uint256[] memory)
+ {
+ return userMemorabilia[user];
+ }

This change ensures gas usage scales with the number of NFTs owned by the user rather than total protocol state, eliminating the denial-of-service vector.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!