Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

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.

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

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

+ 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");
// ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

createMemorabiliaCollection with isActive false for later usage - flow not properly implemented.

Low because an organizer can use it with active = true and organizer is trusted.

Support

FAQs

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