Beatland Festival

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

Global Per-Address Cooldown Prevents Attending Concurrent Performances

Description

  • Users earn BEAT tokens by calling attendPerformance during a live performance window. A 1-hour cooldown (COOLDOWN = 1 hours) prevents users from attending
    performances back-to-back, acting as an anti-farming measure.

  • The cooldown (lastCheckIn[msg.sender]) is a single global timestamp shared across all performances. When the organizer legitimately schedules two performances
    that overlap in time, attending the first one immediately locks the user out of the second for a full hour — even though the second is an entirely different
    event. Users permanently forfeit rewards from the second concurrent performance even while holding a valid pass.

// src/FestivalPass.sol

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");
// ^^ single global timestamp — one attendance blocks ALL other performances for 1 hour

  hasAttended[performanceId][msg.sender] = true;                                                                                                               

@> lastCheckIn[msg.sender] = block.timestamp; // resets cooldown across all performances
...
}

Risk

Likelihood:

  • The organizer can create performances without any restriction on start time overlap — two performances starting simultaneously is a valid and expected scenario
    for a multi-stage festival.

  • Every user who attends the first of two overlapping performances automatically loses eligibility for the second, with no warning or indication at check-in
    time.

Impact:

  • Users lose BEAT rewards they were entitled to for performances they had a valid pass to attend.

  • For a festival with multiple concurrent stages, pass holders can only benefit from one stage per hour regardless of how many passes or how much ETH they spent
    on premium tiers.

Proof of Concept

function test_ConcurrentPerformanceLockout() public {
vm.prank(user1);
festivalPass.buyPass{value: 0.05 ether}(1);

  uint256 startTime = block.timestamp + 1 hours;                                                                                                               

  // Organizer creates two overlapping performances (Stage A and Stage B)                                                                                      
  vm.startPrank(organizer);                             
  uint256 stageA = festivalPass.createPerformance(startTime, 4 hours, 50e18);                                                                                  
  uint256 stageB = festivalPass.createPerformance(startTime, 4 hours, 50e18);                                                                                  
  vm.stopPrank();
                                                                                                                                                               
  vm.warp(startTime + 30 minutes);                      
                                                                                                                                                               
  // User attends Stage A — earns 50e18 BEAT, global cooldown starts                                                                                           
  vm.prank(user1);
  festivalPass.attendPerformance(stageA);                                                                                                                      
  assertEq(beatToken.balanceOf(user1), 50e18);          

  // User tries to attend Stage B immediately — reverts despite being a different event                                                                        
  vm.prank(user1);
  vm.expectRevert("Cooldown period not met");                                                                                                                  
  festivalPass.attendPerformance(stageB); // 50e18 BEAT permanently forfeited
                                                                                                                                                               
  // Stage B ends before cooldown expires
  vm.warp(startTime + 4 hours + 1);                                                                                                                            
  assertFalse(festivalPass.isPerformanceActive(stageB));                                                                                                       
  // Stage B reward is gone forever                                                                                                                            

}

Recommended Mitigation

// src/FestivalPass.sol

  • mapping(address => uint256) public lastCheckIn;

  • mapping(uint256 => mapping(address => uint256)) public lastCheckIn;

    function attendPerformance(uint256 performanceId) external {
    ...

  • require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown period not met");
    
  • require(block.timestamp >= lastCheckIn[performanceId][msg.sender] + COOLDOWN, "Cooldown period not met");                                                  
                                                                                                                                                               
    hasAttended[performanceId][msg.sender] = true;                                                                                                             
    
  • lastCheckIn[msg.sender] = block.timestamp;                                                                                                                 
    
  • lastCheckIn[performanceId][msg.sender] = block.timestamp;
    ...                                                                                                                                                        
    

    }

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!