Beatland Festival

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

Memorabilia collection cannot be re-activated

A memorabilia collection created by FestivalPass::createMemorabiliaCollection function had a isActive param that cannot be set to true when it is set to false the moment it is created

Description

FestivalPass::createMemorabiliaCollection function were supposed to create a new memorabilia collection

But, when an Organizer created a new memorabilia when setting the isActive param as false. the collection cannot be used whatsoever and cannot be re-activated

// Create a new memorabilia collection
function createMemorabiliaCollection(
string memory name,
string memory baseUri,
uint256 priceInBeat,
uint256 maxSupply,
@> bool activateNow
) external onlyOrganizer returns (uint256) {
require(priceInBeat > 0, "Price must be greater than 0");
require(maxSupply > 0, "Supply must be at least 1");
require(bytes(name).length > 0, "Name required");
require(bytes(baseUri).length > 0, "URI required");
uint256 collectionId = nextCollectionId++;
collections[collectionId] = MemorabiliaCollection({
name: name,
baseUri: baseUri,
priceInBeat: priceInBeat,
maxSupply: maxSupply,
currentItemId: 1,
@> isActive: activateNow
emit CollectionCreated(collectionId, name, maxSupply);
return collectionId;
}

Risk

Likelihood:

Everytime when an organizer set the isActive param to false when creating a new Memorabilia collection

Impact:

The collectionId can never be used forever.

Proof of Concept

  1. an Organizer create a new Memorabilia by calling FestivalPass::createMemorabiliaCollection function, but setting the isActive param as false

  2. No one can redeem that collection forever.

Copy the following function into the test/FestivalPass.t.sol::FestivalPassTest test suite contract.

function testCannotUseMemorabilia() public {
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Limited Shirts",
"ipfs://QmShirts",
50e18,
3,
false
);
// Give users BEAT tokens
vm.startPrank(address(festivalPass));
beatToken.mint(user1, 200e18);
beatToken.mint(user2, 200e18);
vm.stopPrank();
// User1 redeems item #1
vm.prank(user1);
vm.expectRevert("Collection not active");
festivalPass.redeemMemorabilia(collectionId);
uint256 token1 = festivalPass.encodeTokenId(collectionId, 1);
assertEq(festivalPass.balanceOf(user1, token1), 0);
}

Recommended Mitigation

Create a function thet used either onlyOwner or onlyOrganizer modifier as the developer see fit, that can set the collection isActive as true or false anytime.

+ function setMemorabiliaStatus(uint256 collectionId, bool isActive) external onlyOrganizer {
+ collections[collectionId].isActive = isActive;
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!