Beatland Festival

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

Performance Overlap Business Logic Flaw Prevents Legitimate Concurrent Attendance

Root + Impact

Description

  • The attendPerformance() function is designed to allow pass holders to attend multiple performances throughout the festival to earn BEAT tokens, with performances potentially running concurrently to provide users with diverse entertainment options. Under normal operation, users should be able to attend any active performance as long as they haven't already attended that specific performance, enabling maximum engagement with overlapping festival events.

  • However, the attendance system implements a global 1-hour cooldown period that prevents users from attending any performance within 1 hour of their last attendance, regardless of whether the performances are different events. This creates a business logic flaw where users are artificially prevented from attending legitimate overlapping performances they should be able to access, reducing their earning opportunities and limiting festival engagement contrary to the intended user experience.

function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
@> require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
@> lastCheckIn[msg.sender] = block.timestamp; // Global timestamp affects ALL performances
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
}
// Cooldown period for check-ins
@> uint256 constant COOLDOWN = 1 hours; // Applied globally across all performances

The vulnerability exists in the global cooldown mechanism where lastCheckIn[msg.sender] is updated for every performance attendance and the COOLDOWN requirement applies universally to all performances. This prevents users from attending different concurrent performances even when such attendance would be legitimate and expected behavior in a festival setting with overlapping events.

Risk

Likelihood:

  • The flaw triggers automatically whenever organizers schedule overlapping performances, which is standard practice for festivals offering concurrent events, workshops, or multi-stage entertainment throughout the day.

  • The issue occurs immediately upon any user attempting to attend multiple performances within the 1-hour window, requiring no special conditions or attack setup - simply normal festival participation with overlapping events.

Impact:

  • Reduced user earning opportunities as pass holders cannot access all performances they paid for, limiting BEAT token acquisition and diminishing the value proposition of higher-tier passes that should provide maximum festival access.

  • Disrupted protocol functionality where the intended festival experience of attending multiple concurrent events is artificially restricted, potentially leading to user dissatisfaction and reduced platform engagement without creating direct financial risks.

Proof of Concept

Recommended Mitigation

The fix modifies the cooldown logic to allow attendance at different performances regardless of timing, while maintaining the cooldown protection against rapid repeated attempts at the same performance. This preserves the anti-spam protection while enabling legitimate concurrent event attendance, restoring the intended festival experience where users can maximize their engagement with overlapping performances.

function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass");
require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
- require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
+ require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN ||
+ !hasAttended[performanceId][msg.sender], "Cooldown period not met");
hasAttended[performanceId][msg.sender] = true;
lastCheckIn[msg.sender] = block.timestamp;
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 25 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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