Beatland Festival

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

Gas-Intensive Loop in `getUserMemorabiliaDetailed` Leads to DoS

Description

  • The getUserMemorabiliaDetailed function is designed to return all the memorabilia NFTs owned by a specific user.

  • It does this by iterating through every single memorabilia collection (cId) and then through every item ever minted within that collection (iId). For each potential token, it performs an ERC1155.balanceOf call to check if the user is the owner. This nested loop structure is highly inefficient.

// src/FestivalPass.sol
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
tokenIds = new uint256[](count);
collectionIds = new uint256[](count);
itemIds = new uint256[](count);
uint256 index = 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) {
tokenIds[index] = tokenId;
collectionIds[index] = cId;
itemIds[index] = iId;
index++;
}
}
}
return (tokenIds, collectionIds, itemIds);
}

Risk

Likelihood:

  • As the festival continues and more memorabilia collections are created and items are sold, the number of iterations in the loops will grow linearly with the total number of NFTs minted across all collections.

Impact:

  • The gas cost of calling getUserMemorabiliaDetailed will increase significantly, eventually exceeding the block gas limit.

  • This will render the function unusable for any user, effectively creating a Denial of Service (DoS) for a core feature of the protocol. Front-ends and other services relying on this function will break.

Proof of Concept

function test_poc_gas_intensive_loop() public {
// 1. Create a large number of collections and mint items to various users.
for (uint256 i = 0; i < 20; i++) {
vm.prank(organizer);
festivalPass.createMemorabiliaCollection("Collection", "ipfs://...", 1, 100, true);
for (uint256 j = 0; j < 50; j++) {
address randomUser = address(uint160(uint256(keccak256(abi.encodePacked(i, j)))));
vm.prank(address(festivalPass));
beatToken.mint(randomUser, 1e18);
vm.startPrank(randomUser);
beatToken.approve(address(festivalPass), 1e18);
festivalPass.redeemMemorabilia(100 + i);
vm.stopPrank();
}
}
// 2. A user tries to view their memorabilia.
// The gas cost for this call will be extremely high due to the nested loops
// iterating through 20 collections * 50 items each = 1000 total items.
// As the numbers grow, this will eventually exceed the block gas limit.
uint256 startGas = gasleft();
(uint256[] memory tokenIds,,) = festivalPass.getUserMemorabiliaDetailed(user1);
uint256 gasUsed = startGas - gasleft();
// This assertion will fail as more collections/items are added.
assertTrue(gasUsed < 1_000_000, "Gas usage is too high!");
}

Recommended Mitigation

// In FestivalPass.sol
+ // Mapping from user address to an array of their owned memorabilia token IDs
+ mapping(address => uint256[]) private userOwnedMemorabilia;
// Redeem a memorabilia NFT from a collection
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
require(collection.currentItemId < collection.maxSupply, "Collection sold out");
// Burn BEAT tokens
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Generate unique token ID
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
// Store edition number
tokenIdToEdition[tokenId] = itemId;
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
+ // Add the token to the user's list
+ userOwnedMemorabilia[msg.sender].push(tokenId);
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
- // Get all memorabilia owned by a user with details
- 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
- tokenIds = new uint256[](count);
- collectionIds = new uint256[](count);
- itemIds = new uint256[](count);
-
- uint256 index = 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) {
- tokenIds[index] = tokenId;
- collectionIds[index] = cId;
- itemIds[index] = iId;
- index++;
- }
- }
- }
-
- return (tokenIds, collectionIds, itemIds);
- }
+ // Get all memorabilia owned by a user with details
+ function getUserMemorabiliaDetailed(address user)
+ external
+ view
+ returns (uint256[] memory tokenIds, uint256[] memory collectionIds, uint256[] memory itemIds)
+ {
+ tokenIds = userOwnedMemorabilia[user];
+ uint256 count = tokenIds.length;
+ collectionIds = new uint256[](count);
+ itemIds = new uint256[](count);
+
+ for (uint256 i = 0; i < count; i++) {
+ (collectionIds[i], itemIds[i]) = decodeTokenId(tokenIds[i]);
+ }
+
+ return (tokenIds, collectionIds, itemIds);
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge 29 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.