Beatland Festival

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

Missing Activation Management for Memorabilia Collections Leads to Permanent Misconfiguration

Missing Activation Management for Memorabilia Collections Leads to Permanent Misconfiguration


Description

The MemorabiliaCollection structure includes an isActive flag that determines whether users are allowed to redeem NFTs from a given collection. This flag is set once during collection creation via the createMemorabiliaCollection function and is later checked in redeemMemorabilia to allow or block redemptions.

However, the protocol does not provide any function to update or toggle the isActive flag after a collection has been created. As a result, the activation state of a collection becomes immutable after initialization.

This design leads to inflexible and potentially irreversible behavior: if a collection is created with isActive = false, it can never be activated; if it is created with isActive = true, it can never be paused or deactivated. The lack of lifecycle management for collections creates a mismatch between the intended use of the isActive flag and the actual functionality implemented in the contract.


Risk

Likelihood: Medium

The likelihood is considered medium because this issue stems from normal usage of the protocol. An organizer can unintentionally create a collection with the wrong isActive value, and there is no mechanism to correct it afterward. While this does not require malicious activity, mistakes during collection creation are plausible, especially when handling multiple collections.

Impact: Medium

The impact is medium because a misconfigured collection can either remain permanently inactive, preventing users from redeeming NFTs, or stay perpetually active, preventing the organizer from pausing or deactivating it when needed. This limits operational control and flexibility, potentially causing lost revenue, user frustration, or difficulties in managing the protocol effectively.


Proof of Concept

The following test demonstrates that if a memorabilia collection is created with isActive set to false, there is no way to activate it later. As a result, the collection becomes permanently unusable and NFTs can never be redeemed from it.

function testCannotActivateCollection() public {
// Organizer creates a collection with isActive set to false
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Test Collection",
"ipfs://testuri/",
10e18,
10,
false
);
// Verify that the collection is indeed inactive after creation
(,,,,, bool active) = festivalPass.collections(collectionId);
assert(active == false);
// A user attempts to redeem an item from the inactive collection
vm.prank(user1);
vm.expectRevert("Collection not active");
festivalPass.redeemMemorabilia(collectionId);
}

To run the test, use the following Foundry command:

forge test --match-test testCannotActivateCollection

Output:

Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] testCannotActivateCollection() (gas: 150189)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.64ms (145.28µs CPU time)
Ran 1 test suite in 9.43ms (1.64ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

This test confirms that once a collection is created with isActive = false, it can never be activated, effectively preventing the collection from ever being redeemed or realized.


Recommended Mitigation

Introduce an explicit function that allows the organizer to update the isActive flag of an existing collection. This function should be restricted with the onlyOrganizer modifier and enable toggling the collection state between active and inactive, restoring proper lifecycle control over memorabilia collections. For example:

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

Lead Judging Commences

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