Beatland Festival

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

Unbounded Iteration Over Global Token Supply in `getUserMemorabiliaDetailed` Can Break Functionality for Active Users

Unbounded Iteration Over Global Token Supply in getUserMemorabiliaDetailed Can Break Functionality for Active Users

Description

  • The getUserMemorabiliaDetailed function is designed to return arrays of token IDs, collection IDs, and item IDs for all memorabilia owned by a specific user.

  • The function exhibits unbounded gas consumption that scales linearly with the number of memorabilia items owned by a user, potentially causing transaction failures or making the function unusable for users with large collections.

function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
) {
// @> First pass: Count owned memorabilia
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++;
}
}
}
// @> Allocate arrays based on count
tokenIds = new uint256[](count);
collectionIds = new uint256[](count);
itemIds = new uint256[](count);
// @> Second pass: Populate arrays
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:

  • Active users who redeem memorabilia regularly will accumulate large collections over time, making this issue inevitable for engaged users

  • The gas cost increases linearly with both the total number of tokens minted globally (nextTokenId) and the user's collection size, affecting all users as the platform grows

Impact:

  • Users with large memorabilia collections will be unable to call this function due to block gas limits, effectively breaking core functionality

  • Frontend applications relying on this function will fail to load user data, creating poor user experience and potential application crashes

  • As the platform scales and more tokens are minted globally, even users with small collections may experience high gas costs

Proof of Concept

function test_GetUserMemorabiliaDetailed_GasConsumption() public {
// Small scale Test (2 collections, 3 items)
vm.startPrank(organizer);
uint256 col1 = festivalPass.createMemorabiliaCollection("Col1", "ipfs://1", 1e18, 100, true);
uint256 col2 = festivalPass.createMemorabiliaCollection("Col2", "ipfs://2", 1e18, 100, true);
vm.stopPrank();
// Give user tokens
vm.prank(address(festivalPass));
beatToken.mint(user1, 100000e18);
// User redeems from both collections (3 items total)
vm.startPrank(user1);
festivalPass.redeemMemorabilia(col1); // Gets item 1 from col1
festivalPass.redeemMemorabilia(col1); // Gets item 2 from col1
festivalPass.redeemMemorabilia(col2); // Gets item 1 from col2
vm.stopPrank();
// Measure gas for small scale
uint256 gasBefore = gasleft();
(uint256[] memory tokenIds1, uint256[] memory collectionIds1, uint256[] memory itemIds1)
= festivalPass.getUserMemorabiliaDetailed(user1);
uint256 gasAfter = gasleft();
uint256 gasUsedSmall = gasBefore - gasAfter;
console.log("=== SMALL SCALE TEST ===");
console.log("Gas used:", gasUsedSmall);
console.log("");
// Verify results
assertEq(tokenIds1.length, 3);
assertEq(collectionIds1[0], col1);
assertEq(itemIds1[0], 1);
assertEq(collectionIds1[1], col1);
assertEq(itemIds1[1], 2);
assertEq(collectionIds1[2], col2);
assertEq(itemIds1[2], 1);
// Large scale Test (10 collections, many items)
vm.startPrank(organizer);
for(uint256 i = 3; i <= 10; i++) {
uint256 newCol = festivalPass.createMemorabiliaCollection(
string(abi.encodePacked("Col", vm.toString(i))),
string(abi.encodePacked("ipfs://", vm.toString(i))),
1e18,
100,
true
);
}
vm.stopPrank();
vm.startPrank(user1);
for (uint256 i = 100; i <= 109; i++) {
for (uint256 j = 0; j < 90; j++) {
festivalPass.redeemMemorabilia(i);
}
}
vm.stopPrank();
// Measure gas for large scale
gasBefore = gasleft();
(uint256[] memory tokenIds3, uint256[] memory collectionIds3, uint256[] memory itemIds3)
= festivalPass.getUserMemorabiliaDetailed(user1);
gasAfter = gasleft();
uint256 gasUsedLarge = gasBefore - gasAfter;
console.log("=== LARGE SCALE TEST ===");
console.log("Gas used:", gasUsedLarge);
console.log("");
}

Recommended Mitigation

+ // Add mapping to track user tokens efficiently
+ mapping(address => uint256[]) private userTokens;
+ // Update userTokens mapping when minting
function _mint(address to, uint256 tokenId) internal override {
super._mint(to, tokenId);
+ userTokens[to].push(tokenId);
}
+ // Update userTokens mapping when transferring
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override {
+ if (from != address(0)) {
+ // Remove from sender's array
+ uint256[] storage fromTokens = userTokens[from];
+ for (uint256 i = 0; i < fromTokens.length; i++) {
+ if (fromTokens[i] == tokenId) {
+ fromTokens[i] = fromTokens[fromTokens.length - 1];
+ fromTokens.pop();
+ break;
+ }
+ }
+ }
+ if (to != address(0)) {
+ // Add to receiver's array
+ userTokens[to].push(tokenId);
+ }
}
function getUserMemorabiliaDetailed(address user) external view returns (
uint256[] memory tokenIds,
uint256[] memory collectionIds,
uint256[] memory itemIds
) {
- uint256 balance = balanceOf(user);
- tokenIds = new uint256[](balance);
- collectionIds = new uint256[](balance);
- itemIds = new uint256[](balance);
-
- uint256 index = 0;
- for (uint256 tokenId = 1; tokenId <= nextTokenId; tokenId++) {
- if (ownerOf(tokenId) == user) {
- tokenIds[index] = tokenId;
- collectionIds[index] = memorabiliaTokens[tokenId].collectionId;
- itemIds[index] = memorabiliaTokens[tokenId].itemId;
- index++;
- }
- }
+ uint256[] storage userTokenIds = userTokens[user];
+ uint256 length = userTokenIds.length;
+
+ tokenIds = new uint256[](length);
+ collectionIds = new uint256[](length);
+ itemIds = new uint256[](length);
+
+ for (uint256 i = 0; i < length; i++) {
+ uint256 tokenId = userTokenIds[i];
+ tokenIds[i] = tokenId;
+ collectionIds[i] = memorabiliaTokens[tokenId].collectionId;
+ itemIds[i] = memorabiliaTokens[tokenId].itemId;
+ }
}
Updates

Lead Judging Commences

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