Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: low
Valid

Memorabilia collections created with `activateNow = false` are permanently stuck and unusable

Memorabilia collections created with activateNow = false are permanently stuck and unusable

Description

The FestivalPass::createMemorabiliaCollection function accepts an activateNow parameter that sets the collection's isActive status. However, there is no function to change this status after creation.

If a collection is created with activateNow = false (e.g., to prepare it before a scheduled release), it can never be activated, rendering the collection permanently unusable. Users cannot redeem memorabilia from it because redeemMemorabilia requires isActive to be true.

function createMemorabiliaCollection(
string memory name,
string memory baseUri,
uint256 priceInBeat,
uint256 maxSupply,
bool activateNow
) external onlyOrganizer returns (uint256) {
// ... validation ...
collections[collectionId] = MemorabiliaCollection({
name: name,
baseUri: baseUri,
priceInBeat: priceInBeat,
maxSupply: maxSupply,
currentItemId: 1,
@> isActive: activateNow // No function exists to change this after creation
});
// ...
}
function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
@> require(collection.isActive, "Collection not active"); // Blocks redemption if inactive
// ...
}

Risk

Likelihood:

  • This will occur when the organizer creates a collection with activateNow = false, intending to activate it later for a scheduled release.

Impact:

  • The collection is permanently locked and no memorabilia can ever be redeemed from it.

  • The collectionId is consumed and cannot be reused, wasting a slot in the collection registry.

  • Organizer must redeploy or create a new collection, causing confusion and potential loss of marketing/coordination efforts around the original collection ID.

Proof of Concept

  1. Organizer creates a memorabilia collection with activateNow = false

  2. Organizer attempts to activate the collection later but discovers no activation function exists

  3. Users attempt to redeem memorabilia but transactions revert with "Collection not active"

  4. Collection is permanently unusable

Add the following test to your FestivalPass.t.sol file:

function testInactiveCollectionCannotBeActivated() public {
// Organizer creates collection with activateNow = false
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Poster",
"ipfs://poster",
100e18, // 100 BEAT
50, // max supply
false // NOT activated
);
// Give user BEAT tokens and approval
address user = makeAddr("user");
vm.prank(address(festivalPass));
beatToken.mint(user, 1000e18);
vm.prank(user);
beatToken.approve(address(festivalPass), type(uint256).max);
// User tries to redeem - reverts because collection is inactive
vm.prank(user);
vm.expectRevert("Collection not active");
festivalPass.redeemMemorabilia(collectionId);
// No function exists to activate the collection
// Collection is permanently stuck
}

Recommended Mitigation

Add a function to toggle collection activation status:

Note: Add the event CollectionStatusUpdated(uint256 indexed collectionId, bool isActive) to IFestivalPass.sol

+ function setCollectionActive(uint256 collectionId, bool active) external onlyOrganizer {
+ require(collections[collectionId].priceInBeat > 0, "Collection does not exist");
+ collections[collectionId].isActive = active;
+ emit CollectionStatusUpdated(collectionId, active);
+ }.
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 22 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-01] Inactive Collections — Indefinite BEAT Lock-up

# Inactive Collections — Indefinite BEAT Lock-up ## Description * Normal behaviour: Organizer creates memorabilia collections with `activateNow = true` so users can immediately redeem BEAT for NFTs. * Issue: Collections can be created with `activateNow = false` and there is **no mechanism** to activate them later, nor any timeout. Users may acquire BEAT expecting to redeem specific memorabilia, but the organizer can indefinitely prevent access. ```solidity function createMemorabiliaCollection(..., bool activateNow) external onlyOrganizer { // ... validation ... @> collections[collectionId] = MemorabiliaCollection({ // ... isActive: activateNow // Can be false forever }); } function redeemMemorabilia(uint256 collectionId) external { @> require(collection.isActive, "Collection not active"); // Permanent block // ... } ``` ## Risk **Likelihood**: * Organizer may create collections in advance but forget to activate. * Intentional strategy to create hype then indefinitely delay launch. **Impact**: * Users hold BEAT tokens anticipating memorabilia that never becomes available. * Economic utility of BEAT reduced if major collections remain locked. ## Proof of Concept ```solidity function test_CollectionNeverActivated() public { // Alice gets BEAT tokens vm.prank(alice); festivalPass.buyPass{value: 0.1 ether}(VIP_PASS); // gets 5 BEAT bonus // Organizer creates inactive collection vm.prank(organizer); uint256 collectionId = festivalPass.createMemorabiliaCollection( "Limited Edition", "ipfs://limited", 3e18, 100, false // NOT activated ); // Alice tries to redeem but can't vm.prank(alice); vm.expectRevert("Collection not active"); festivalPass.redeemMemorabilia(collectionId); // Time passes, organizer chooses not to activate vm.warp(block.timestamp + 365 days); // Alice still can't redeem - funds effectively locked vm.prank(alice); vm.expectRevert("Collection not active"); festivalPass.redeemMemorabilia(collectionId); assertEq(beatToken.balanceOf(alice), 5e18, "Alice holds 'useless' BEAT"); } ``` ## Recommended Mitigation ```diff + mapping(uint256 => bool) public collectionActivated; + function activateCollection(uint256 collectionId) external onlyOrganizer { + require(collections[collectionId].priceInBeat > 0, "Collection does not exist"); + collections[collectionId].isActive = true; + collectionActivated[collectionId] = true; + } // Or add automatic timeout: + uint256 constant ACTIVATION_DEADLINE = 30 days; + mapping(uint256 => uint256) public collectionCreatedAt; function createMemorabiliaCollection(...) external onlyOrganizer { // ... + collectionCreatedAt[collectionId] = block.timestamp; } function redeemMemorabilia(uint256 collectionId) external { MemorabiliaCollection storage collection = collections[collectionId]; + bool autoActive = block.timestamp >= collectionCreatedAt[collectionId] + ACTIVATION_DEADLINE; + require(collection.isActive || autoActive, "Collection not active"); // ... } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!