Beatland Festival

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

activateNow immutable post-deploy

Root + Impact

No setter function exists for the activateNow boolean flag

Description

The activateNow boolean flag controls whether the contract's core functionality — likely pass minting or purchasing — is currently active. This flag is set once in the constructor at deployment time and is never subsequently modifiable, because the contract contains no owner-gated or admin-gated setter function to update it.

This design creates a rigid on/off state that cannot adapt to real-world operational needs. If the contract is deployed with activateNow = true, there is no way to pause it in response to a discovered vulnerability, unexpected market conditions, or regulatory concerns — standard operational safeguards that every production contract should have. Conversely, if deployed with activateNow = false, there is no way to ever activate it, rendering the contract permanently non-functional.

While this is not an exploitable vulnerability in the traditional sense — an attacker cannot manipulate activateNow — the absence of an activation toggle removes a critical safety lever. Industry-standard contracts (including OpenZeppelin's Pausable module) treat pausability as a non-negotiable safety feature precisely because emergencies are unpredictable. A contract that cannot be paused is a contract that cannot be protected once deployed.

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++; // start from 101
// @ans what happen on collectionId 1 - 100 ???
// @ans collentionId 1-3 => pass
collections[collectionId] = MemorabiliaCollection({
name: name,
baseUri: baseUri,
priceInBeat: priceInBeat,
maxSupply: maxSupply,
currentItemId: 1, // Start item IDs at 1
// @ans this will always 1?
// @ans what if the itemId == 0 => i guess nothing happen
isActive: activateNow
// @ans is there a way to activate or deactivae it? => nope, once activate, it cannot be deactivate
// @audit low activateNow is not used, if the owner want to deactivate it, the contract did not have function to do that
});
emit CollectionCreated(collectionId, name, maxSupply);
return collectionId;
}

Risk

Likelihood:

This vulnerability is rated Low likelihood as an exploitable attack vector — no external party can change the flag maliciously. However, the operational risk is near-certain to manifest at some point: virtually every production contract eventually needs an emergency pause. The likelihood that the missing setter will become a problem during the contract's lifetime is high, even if the probability of a targeted exploit is low.

Impact:

This vulnerability is rated Medium impact. If the contract is deployed in the wrong activation state, it is either permanently unusable or permanently un-pausable. In the event of a security incident elsewhere in the system, the inability to pause the pass contract could allow continued exploitation while the team scrambles to deploy a replacement. The redeployment cost — including user migration, loss of minted token history, and reputational damage — is significant. This is classified as medium rather than critical because the flag itself does not control fund custody, only access.

Proof of Concept

The proof of concept is straightforward: after deployment with activateNow = false, there is no transaction the owner can send to enable the contract. Any user interaction gated behind the require(activateNow) check will revert indefinitely. The contract must be redeployed to change this state, which forfeits any existing state such as previously registered buyers or configuration.

// At deployment:
constructor() {
activateNow = false; // contract is inactive
}
// No function to change this exists.
// Owner tries to activate:
// contract.setActivate(true) ← function does not exist → revert
// Result: contract is permanently inactive.
// All buy() calls revert at:
require(activateNow, "Not active");

Recommended Mitigation

At minimum, add an onlyOwner setter that updates activateNow and emits an event for off-chain indexers. The event is important — it allows explorers and monitoring tools to track activation history and alert on unexpected state changes. The strongly recommended approach is to replace the custom flag entirely with OpenZeppelin's Pausable module, which is audited, well-understood, and integrates cleanly with whenNotPaused and whenPaused modifiers on any function that needs to respect the activation state.

// Add an owner-only setter with event emission:
address public owner;
bool public activateNow;
event ActivationChanged(bool newState);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor(bool _activateNow) {
owner = msg.sender;
activateNow = _activateNow;
}
function setActivateNow(bool _state) external onlyOwner {
activateNow = _state;
emit ActivationChanged(_state);
}
// RECOMMENDED — use OpenZeppelin Pausable for battle-tested pattern:
import "@openzeppelin/contracts/security/Pausable.sol";
contract PassContract is Pausable {
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function buy() external whenNotPaused { ... }
}
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!