Beatland Festival

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

H-01: Global 1-hour Cooldown Enables Systematic Reward Farming

Description:

The attendPerformance() function uses a global lastCheckIn mapping that applies a 1-hour cooldown across ALL performances. This creates a systemic vulnerability where users can farm rewards from multiple performances with only a single 1-hour wait, rather than per-performance cooldowns.

Attack Vector:

  1. User attends Performance A → receives 10 BEAT (base) × multiplier

  2. User waits 1 hour (global cooldown)

  3. User attends Performance B → receives 10 BEAT (base) × multiplier

  4. Repeat across all performances

Risk + Impact

  • With 10 performances: 100+ BEAT per hour

  • With VIP pass (2x): 200+ BEAT per hour

  • With BACKSTAGE pass (3x): 300+ BEAT per hour

Root Cause:

lastCheckIn is a global mapping (line 30) rather than per-performance tracking.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {FestivalPass} from "../src/FestivalPass.sol";
import {BeatToken} from "../src/BeatToken.sol";
contract POC_AllFindings is Test {
FestivalPass public festival;
BeatToken public beat;
address public owner;
address public organizer;
address public attacker;
address public user1;
address public user2;
uint256 constant GENERAL_PRICE = 0.05 ether;
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
function setUp() public {
owner = address(this);
organizer = makeAddr("organizer");
attacker = makeAddr("attacker");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
beat = new BeatToken();
festival = new FestivalPass(address(beat), organizer);
beat.setFestivalContract(address(festival));
vm.startPrank(organizer);
festival.configurePass(1, GENERAL_PRICE, 5000);
festival.configurePass(2, VIP_PRICE, 1000);
festival.configurePass(3, BACKSTAGE_PRICE, 100);
vm.stopPrank();
vm.deal(attacker, 100 ether);
vm.deal(user1, 100 ether);
vm.deal(user2, 100 ether);
}
function test_POC_H01_CooldownFarming() public {
// Step 1: Buy BACKSTAGE pass (3x multiplier)
vm.prank(attacker);
festival.buyPass{value: BACKSTAGE_PRICE}(3);
uint256 baseReward = 100e18;
// Step 2: Create 10 performances, each 2 hours apart
uint256 startTime = block.timestamp + 1 hours;
vm.startPrank(organizer);
for (uint256 i = 0; i < 10; i++) {
festival.createPerformance(startTime + i * 2 hours, 1 hours, baseReward);
}
vm.stopPrank();
// Step 3: Attend each performance (1 hour cooldown between each)
uint256 totalEarned;
for (uint256 i = 0; i < 10; i++) {
uint256 perfTime = startTime + i * 2 hours + 30 minutes;
vm.warp(perfTime);
vm.prank(attacker);
festival.attendPerformance(i);
totalEarned += baseReward * 3; // 3x multiplier
}
// Step 4: Verify attacker earned rewards from ALL 10 performances
assertEq(beat.balanceOf(attacker), totalEarned + 15e18); // 15 BEAT welcome bonus
console.log("=== H-01: COOLDOWN FARMING EXPLOIT ===");
console.log("Attacker earned", totalEarned / 1e18, "BEAT from 10 performances");
console.log("Cost: 0.25 ETH for BACKSTAGE pass");
console.log("Reward rate:", (totalEarned / 1e18) * 4 / 100, "BEAT per ETH (at $3000/ETH)");
console.log("VULNERABILITY: Global 1-hour cooldown allows farming across all performances");
}

Recommended Mitigation

Use per-performance cooldown tracking: mapping(uint256 => mapping(address => uint256)) public lastCheckInByPerformance;

Updates

Lead Judging Commences

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