Beatland Festival

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

# I-1: One-shot BEAT↔festival binding is irreversible

Root + Impact

BeatToken.setFestivalContract can be called exactly once and can never be changed. The BEAT token is therefore permanently bound to a single FestivalPass deployment. If the pass contract needs to be replaced (bug fix, migration, new festival season), the existing BEAT cannot be repointed, and a new festival requires a brand-new BEAT token with no carry-over of balances. The code flags this itself with an //@audit comment.

Description

// src/BeatToken.sol:13-16
function setFestivalContract(address _festival) external onlyOwner {
@> require(festivalContract == address(0), "Festival contract already set"); //@audit cannot be reused for other festivals
festivalContract = _festival;
}

The paired write in FestivalPass (beatToken = _beatToken, :47) has no setter either, so the binding is two-sided and immutable.

Risk

Likelihood: Low. It only matters if the festival contract must be migrated or the initial binding is wrong (see L-2).

Impact: If the festival contract is compromised or must be upgraded, a new BEAT token must be deployed and all balances/allowances abandoned. Users holding BEAT against the old festival lose the ability to redeem memorabilia through a new deployment. Not exploitable by an external attacker; a design limitation.

Proof of Concept

The existing test suite already covers this. Setting the festival contract twice (to different addresses) reverts on the second call.

// test/BeatToken.t.sol (existing)
function testFuzz_SetFestivalContract_CannotOverwrite(address _first, address _second) public {
vm.assume(_first != address(0)); vm.assume(_second != address(0)); vm.assume(_first != _second);
beatToken.setFestivalContract(_first);
vm.expectRevert("Festival contract already set"); // one-shot latch
beatToken.setFestivalContract(_second);
assertEq(beatToken.festivalContract(), _first);
}

Recommended Mitigation

If migration is a real requirement, allow the owner to repoint the festival contract under a delay or with the old contract's consent, and guard against zero. If immutability is intended, document it explicitly and ensure the binding is correct at deployment.

+ // Option A — keep the one-shot latch but reject zero and document immutability
function setFestivalContract(address _festival) external onlyOwner {
+ require(_festival != address(0), "zero festival");
require(festivalContract == address(0), "Festival contract already set");
festivalContract = _festival;
}
​
+ // Option B — allow a bounded, owner-controlled migration if upgrades are expected
+ function migrateFestivalContract(address _festival) external onlyOwner {
+ require(_festival != address(0), "zero festival");
+ festivalContract = _festival;
+ }

(If migration must remain impossible, keep the latch and add NatSpec stating the one-shot lifetime binding so integrators do not assume upgradability. If migration is allowed, gate it behind a timelock or the old contract's consent.)

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours 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!