Beatland Festival

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

Denial of Service (DoS) in getUserMemorabiliaDetailed due to Unbounded Gas Consumption


Description

The getUserMemorabiliaDetailed function uses non-scalable, nested for loops that iterate through every memorabilia token in the system to find those owned by a user. As protocol usage grows, the gas cost of this function is guaranteed to exceed the block limit, causing a permanent Denial of Service (DoS) and breaking a core protocol feature.

Impact

The impact is a permanent Denial of Service (DoS) on the getUserMemorabiliaDetailed function. This breaks any front-end component that relies on it, preventing users from viewing their assets and severely degrading the platform's user experience and overall usability.


Risk

Likelihood: (High)

  • The likelihood is High. The vulnerability will inevitably be triggered under normal protocol usage. As more memorabilia are minted over time, it is certain the function's gas cost will exceed the block limit.

Impact: (Medium)

  • The impact is Medium. This DoS vulnerability breaks a core feature, preventing users from viewing their assets on any front-end. While not a loss of funds, it severely damages the platform's usability and trustworthiness.


Proof of Concept for Denial of Service (DoS) in test_DoS_GetUserMemorabiliaDetaile()

Overview: The getUserMemorabiliaDetailed function uses inefficient nested loops that iterate through all system tokens. This non-scalable design guarantees that as protocol usage grows, the function's gas cost will exceed the block limit, causing a permanent Denial of Service (DoS).

Actors :

  • Attacker: No special attacker is needed. The vulnerability is triggered by normal protocol usage over time.

  • Victim: Any user of the front-end application who wants to view their collected memorabilia.

  • Protocol: The FestivalPass contract, whose getUserMemorabiliaDetailed function becomes permanently unusable.

// This test should be added to test/FestivalPass.t.sol
function test_DoS_GetUserMemorabiliaDetailed() public {
// --- SETUP ---
// Define the number of collections to simulate a grown ecosystem.
uint256 collectionCount = 200;
// Line 1: As the Organizer, create 200 memorabilia collections.
vm.startPrank(organizer);
for (uint256 i = 0; i < collectionCount; i++) {
festivalPass.createMemorabiliaCollection("Test", "ipfs://", 1 ether, 100, true);
}
// Line 2: Create a performance with a large BEAT reward to fund the user.
uint256 hugeReward = collectionCount * 1 ether;
uint256 performanceId = festivalPass.createPerformance(block.timestamp + 1, 1 hours, hugeReward);
// Line 3: Configure the price for the general pass.
festivalPass.configurePass(1, 0.1 ether, 1000);
vm.stopPrank();
// Line 4: Prepare a test user.
address user = makeAddr("user");
// Line 5: Advance the blockchain time to make the performance active.
vm.warp(block.timestamp + 1);
// Line 6: As the user, buy a pass and attend the performance to earn BEAT tokens.
vm.startPrank(user);
festivalPass.buyPass{value: 0.1 ether}(1);
festivalPass.attendPerformance(performanceId);
// Line 7: Approve the festival contract to spend the user's BEAT tokens.
beatToken.approve(address(festivalPass), type(uint256).max);
vm.stopPrank();
// --- EXPLOIT ---
// Line 8: The user redeems one memorabilia item from each of the 200 collections.
vm.startPrank(user);
for (uint256 i = 0; i < collectionCount; i++) {
uint256 collectionId = 100 + i;
festivalPass.redeemMemorabilia(collectionId);
}
vm.stopPrank();
// --- VERIFICATION ---
// Line 9: Call the vulnerable function. This call will fail due to out of gas.
// The test failing here is the proof of the vulnerability.
(uint256[] memory tokenIds,,) = festivalPass.getUserMemorabiliaDetailed(user);
// Line 10: This assertion will not be reached, confirming the failure of the line above.
assertEq(tokenIds.length, collectionCount);
}

Instructions to Run PoC

  • Set up the project environment:

git clone https://github.com/CodeHawks-Contests/2025-07-beatland-festival.git
cd 2025-07-beatland-festival
forge install
  • Add the Proof of Concept test case:

    Copy the complete test_DoS_GetUserMemorabiliaDetailed function provided in the "Working Test Case" section and append it to the end of the test/FestivalPass.t.sol file.

  • Run the specific test:

    Execute the following command in your terminal. This command will compile the contracts and run only the PoC test for this vulnerability.

forge test --match-test test_DoS_GetUserMemorabiliaDetailed -vv

Expected Output (gas: 28,474,534)

git:(main*)$ forge test --match-test test_DoS_GetUserMemorabiliaDetailed -vv
[⠊] Compiling...
[⠊] Compiling 1 files with Solc 0.8.25
[⠒] Solc 0.8.25 finished in 932.09ms
Compiler run successful:
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[FAIL: EvmError: Revert] test_DoS_GetUserMemorabiliaDetailed() (gas: 28474534)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 27.08ms (10.15ms CPU time)
Ran 1 test suite in 99.49ms (27.08ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/FestivalPass.t.sol:FestivalPassTest
[FAIL: EvmError: Revert] test_DoS_GetUserMemorabiliaDetailed() (gas: 28474534)
Encountered a total of 1 failing tests, 0 tests succeeded

Recommended Mitigation

The vulnerability's root cause is the inefficient iteration through all possible token IDs. The recommended solution is to implement a tracking system that maps users directly to their owned tokens, eliminating the need for gas-intensive loops.

The following changes should be applied to FestivalPass.sol:

1 - Add a new state variable to track owned tokens for each user.

// src/FestivalPass.sol
// Memorabilia collections
mapping(uint256 => MemorabiliaCollection) public collections; // collectionId => Collection
mapping(uint256 => uint256) public tokenIdToEdition; // tokenId => edition number
+ mapping(address => uint256[]) private _userOwnedMemorabilia;
modifier onlyOrganizer() {

2 - Update the redeemMemorabilia function to populate the new tracking array upon minting.

// src/FestivalPass.sol
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
+ _userOwnedMemorabilia[msg.sender].push(tokenId);
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}

3 - Replace the vulnerable getUserMemorabiliaDetailed function with a highly efficient one that simply returns the tracked data.

// src/FestivalPass.sol
- // 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
- ) {
- // ... old, inefficient code with loops ...
- }
+ function getUserMemorabilia(address user) external view returns (uint256[] memory) {
+ return _userOwnedMemorabilia[user];
+ }
Updates

Lead Judging Commences

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