Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Permanent Festival Contract Lock Without Upgrade Path

Root + Impact

My Github Link

https://github.com/bonave/Festival-Contract-Lock-PoC.git

Description

The BEAT contract inherits "AccessControl" mechanism but does not expose or properly implement role lifecycle management functions (grant), which makes the FESTIVAL_ROLE irrevocable once assigned in the contract.


Proposed fix to root cause which creates a permanant one way lock to zero implementation in the festival contract

require(festivalContract == address(0),

Risk

Likelihood:

  • Token Economy loss : Holders of beat token could lose 100% of their holdings/shares

  • Irreversable Privilidged Escalation: The attacker gain acces to the token contract and Festival roles priviledges, thereby burning users tokens without approval


Impact:

The festival contract is compromised, contain bugs and the token contract is permanently unusable. Therefore the contract upgrade will be effected. All BEAT tokens will be worthless as no new festival contract can be set to restore functionality. The token economy could fail as a result of this vulnerability.

Proof of Concept

This PoC demonstrates the risk of permanently locking a contract reference
due to a one-time set function without upgrade capabilites


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {IERC20} from "openzeppelin-contracts/token/ERC20/IERC20.sol";
import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import {AccessControl} from "openzeppelin-contracts/access/AccessControl.sol";
import {ERC20Burnable} from "openzeppelin-contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
/** @title PoC for Permanent Lock of Festival Contract
@author Security Researcher
@notice This PoC demonstrates the risk of permanently locking a contract reference
due to a one-time set function without upgrade capabilities.
@dev This is inspired by the FestivalLock issue in Festival.sol
*/
// Vulnerable Ticket System Contract
contract VulnerableTicketSystem {
address public festivalContract;
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only Owner");
_;
}
function setFestivalContract(address _festival) external onlyOwner {
require(festivalContract == address(0), "Festival contract already set"); //@audit cannot be reused for other festivals
festivalContract = _festival;
}
}
// Malicious/Wrong Festival Contract
contract MaliciousFestival {
function exploit() external pure returns (string memory) {
return "This is a malicious contract";
}
}
// Upgraded Festival Contract
contract UpgradedFestival {
function newFeature() external pure returns (string memory) {
return "This is an upgraded festival contract";
}
}
// Test Contract to demonstrate the PoC
contract FestivalLockPoC is Test {
VulnerableTicketSystem public ticketSystem;
MaliciousFestival public maliciousFestival;
UpgradedFestival public upgradedFestival;
address public owner;
function setUp() public {
owner = address(this);
ticketSystem = new VulnerableTicketSystem();
maliciousFestival = new MaliciousFestival();
upgradedFestival = new UpgradedFestival();
}
// @notice Test to demonstrate permanent lock of festival contract
function testPermanentFestivalLock() public {
// Step 1: Owner sets malicious/wrong festival contract
console.log("Step 1: Owner sets malicious festival contract");
ticketSystem.setFestivalContract(address(maliciousFestival));
console.log("Festival contract set to:", ticketSystem.festivalContract());
// Step 2: Attempt to upgrade to a new festival contract
console.log("Step 2: Attempting to upgrade to a new festival contract");
try ticketSystem.setFestivalContract(address(upgradedFestival)) {
console.log("Upgrade succeeded unexpectedly!");
} catch {
console.log("Upgrade failed as expected: Festival contract is permanently locked.");
}
// Step 3: Verify the festival contract remains the malicious one
console.log("Step 3: Verifying the festival contract remains unchanged");
require(ticketSystem.festivalContract() == address(maliciousFestival), "Festival contract should remain the malicious one");
console.log("Festival contract is still:", ticketSystem.festivalContract());
console.log("Test completed successfully.");
}
}

Recommended Mitigation

The festival contract mitigation provides total protection against the vulnerability thereby maintaining flexibility for upgrades, security checks and business evolution process.

- remove this code
function setFestivalContract(address _festival) external onlyOwner {
require(festivalContract == address(0), "Festival contract already set"); //@audit cannot be reused for other festivals
festivalContract = _festival;
}
+add this code
// Upgradeable Ticket System Contract with Timelock
contract UpgradeableTicketSystem is Ownable {
address public festivalContract;
event FestivalContractUpgradeProposed(address indexed newContract, uint256 execute after);
event FestivalContractUpgraded(address indexed newContract);
uint256 public constant UPGRADE_TIMELOCK = 1 days;
address public pendingFestivalContract;
uint256 public upgradeTimestamp;
function proposedUpgrade(address _newFestivalContract) external onlyOwner {
require(_newFestivalContract != address(0), "Invalid festival address");
pendingFestivalContract = _newFestivalContract;
upgradeTimestamp = block.timestamp + UPGRADE_TIMELOCK;
emit FestivalContractUpgradeProposed(_newFestivalContract, upgradeTimestamp);
}
function executeUpgrade() external onlyOwner {
require(pendingFestivalContract != address(0), "No upgrade proposed");
require(block.timestamp >= upgradeTimestamp, "Upgrade timelock not expired");
festivalContract = pendingFestivalContract;
pendingFestivalContract = address(0);
upgradeTimestamp = 0;
emit FestivalContractUpgraded(festivalContract);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 8 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!