Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: medium
Invalid

L03. Single-Use Festival Contract Restriction Limits Token Reusability and Upgradeability

Description

  • The BeatToken contract is a custom ERC20 used to distribute and burn tokens for a festival ecosystem. It includes a setFestivalContract() function, which allows the token to authorize one FestivalPass contract for minting and burning BEAT tokens.

  • However, this function uses a hard require condition to prevent any updates once the festivalContract is set. This design is reinforced by a comment in the code:

    require(festivalContract == address(0), "Festival contract already set"); //@audit cannot be reused for other festivals


  • This enforces single-use behavior and prevents reusing the same token contract for future festivals, seasonal upgrades, or contract migrations. If the current FestivalPass contract becomes outdated, the BEAT token becomes locked to that logic, forcing the organizer to deploy a new token, fragmenting balances and user trust.

function setFestivalContract(address _festival) external onlyOwner {
require(festivalContract == address(0), "Festival contract already set"); //@> cannot be reused for other festivals
festivalContract = _festival;
}

Risk

Likelihood:

  • Happens when organizers wish to upgrade to a new FestivalPass contract for a new season or fix vulnerabilities.

  • Happens when the token is expected to outlive a specific contract (e.g., to support multiple festivals or future integrations).

Impact:

  • Limits the ability to maintain and upgrade the ecosystem over time.

  • Causes token fragmentation, requiring airdrops or manual token migrations.

  • Breaks user continuity for holders of BEAT tokens across seasons or dApps.


Proof of Concept

BeatToken beat = new BeatToken();
beat.setFestivalContract(address(festivalV1)); // Works
beat.setFestivalContract(address(festivalV2)); // Reverts: "Festival contract already set"

Organizers cannot reuse the BEAT token for a future upgraded festival logic, forcing them to deploy a new incompatible token.


Recommended Mitigation

Example 1:

Implement an owner-controlled update mechanism that allows the authorized festival contract to be updated securely.
Explanation:

  • setFestivalContract() ensures safe one-time setup of the initial logic.

  • updateFestivalContract() gives the owner controlled upgrade flexibility while maintaining traceability via events.

  • This preserves security while avoiding token lock-in or re-deployment costs.

/// @notice Can be called only once to set the initial contract
function setFestivalContract(address _festival) external onlyOwner {
require(festivalContract == address(0), "Festival contract already set");
require(_festival != address(0), "Invalid address");
festivalContract = _festival;
emit FestivalContractSet(_festival);
}
/// @notice Allows the owner to update the contract in future
function updateFestivalContract(address _newFestival) external onlyOwner {
require(festivalContract != address(0), "Festival contract not already set");
require(_newFestival != address(0), "Invalid address");
address old = festivalContract;
festivalContract = _newFestival;
emit FestivalContractUpdated(old, _newFestival);
}

Benefits of this approach:

  • Still restricts minting and burning to one contract at a time.

  • Allows the owner to update it when necessary (e.g., in case of upgrades or bug fixes).

  • Emits an event to track updates.

  • Maintains forward compatibility and reduces token fragmentation.

    Example 2

    Use a whitelist pattern to allow multiple trusted festival contracts to interact with the token, controlled by the owner.

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.25;
    contract BeatToken is ERC20, Ownable2Step {
    mapping(address => bool) public authorizedFestivals;
    event FestivalAuthorized(address indexed festival);
    event FestivalRevoked(address indexed festival);
    constructor() ERC20("BeatDrop Token", "BEAT") Ownable(msg.sender) {}
    /// @notice Owner can authorize a festival contract to mint/burn
    function authorizeFestival(address _festival) external onlyOwner {
    require(_festival != address(0), "Invalid address");
    require(!authorizedFestivals[_festival], "Already authorized");
    authorizedFestivals[_festival] = true;
    emit FestivalAuthorized(_festival);
    }
    /// @notice Owner can revoke a festival contract
    function revokeFestival(address _festival) external onlyOwner {
    require(authorizedFestivals[_festival], "Not authorized");
    authorizedFestivals[_festival] = false;
    emit FestivalRevoked(_festival);
    }
    function mint(address to, uint256 amount) external {
    require(authorizedFestivals[msg.sender], "Only_Festival_Mint");
    _mint(to, amount);
    }
    function burnFrom(address from, uint256 amount) external {
    require(authorizedFestivals[msg.sender], "Only_Festival_Burn");
    _burn(from, amount);
    }



    authorizeFestival() allows the owner to add a new approved contract (e.g., for future festivals or upgrades).

  • revokeFestival() enables access control and deactivation.

  • mint() and burnFrom() now enforce that only authorized contracts can interact with token supply.

  • The system supports safe upgradeability and multi-festival use, while still being owner-controlled.

Updates

Lead Judging Commences

inallhonesty Lead Judge 27 days ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

`setFestivalContract` only callable once

This is intended. It's done like that because the festival contract requires beat token's address and vice versa.

Support

FAQs

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