Description
The attendPerformance function implements a global 1-hour cooldown that becomes problematic when performances have durations shorter than the cooldown period. Users who attend a performance shorter than 1 hour cannot attend subsequent performances until the full cooldown expires, effectively locking them out of multiple short events and reducing the utility of their festival passes.
Root Cause
The system enforces a fixed 1-hour cooldown but allows creation of performances with any duration, including those shorter than the cooldown period:
uint256 constant COOLDOWN = 1 hours;
function createPerformance(uint256 startTime, uint256 duration, uint256 reward) external onlyOrganizer {
require(duration > 0, "Duration must be greater than 0");
}
require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
Risk
Likelihood: Medium - Organizers may create short performances (30-45 minutes) without realizing the cooldown implications.
Impact: Medium - Users miss earning opportunities from short performances, but long performances (≥1 hour) remain accessible.
Impact
Medium severity because:
-
Users attending short performances cannot participate in subsequent events until cooldown expires
-
Multiple short performances in sequence become effectively inaccessible
-
BEAT token earning opportunities are lost for legitimate users
-
Organizers may unknowingly create unusable short performance schedules
Proof of Concept
This test demonstrates how users are locked out of multiple short performances due to the global cooldown mechanism, missing significant BEAT earning opportunities:
function test_ShortPerformancesCooldownIssue() public {
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
uint256 baseTime = block.timestamp + 1 hours;
vm.startPrank(organizer);
uint256 perf1 = festivalPass.createPerformance(baseTime, 30 minutes, 50e18);
uint256 perf2 = festivalPass.createPerformance(baseTime + 30 minutes, 30 minutes, 75e18);
uint256 perf3 = festivalPass.createPerformance(baseTime + 60 minutes, 30 minutes, 100e18);
vm.stopPrank();
vm.warp(baseTime + 15 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perf1);
assertEq(beatToken.balanceOf(user1), 50e18);
vm.warp(baseTime + 30 minutes);
vm.prank(user1);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perf2);
vm.warp(baseTime + 60 minutes);
vm.prank(user1);
vm.expectRevert("Cooldown period not met");
festivalPass.attendPerformance(perf3);
assertEq(beatToken.balanceOf(user1), 50e18);
}
Recommended Mitigation
Add validation to ensure performances are long enough to work with the cooldown system:
function createPerformance(
uint256 startTime,
uint256 duration,
uint256 reward
) external onlyOrganizer returns (uint256) {
require(startTime > block.timestamp, "Start time must be in the future");
require(duration > 0, "Duration must be greater than 0");
+ require(duration >= COOLDOWN, "Performance duration must be at least 1 hour");
performances[performanceCount] = Performance({
startTime: startTime,
endTime: startTime + duration,
baseReward: reward
});
emit PerformanceCreated(performanceCount, startTime, startTime + duration);
return performanceCount++;
}