Beatland Festival

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

[H-2] FestivalPass::getMultiplier logic flaw allows unlimited reward exploitation

[H-2] FestivalPass::getMultiplier logic flaw allows unlimited reward exploitation

Description:

The FestivalPass::getMultiplier function uses a simple if-else hierarchy that only checks for the presence of pass types, not their quantity or intended usage.

// Get user's reward multiplier based on pass type
function getMultiplier(address user) public view returns (uint256) {
if (balanceOf(user, BACKSTAGE_PASS) > 0) {
return 3; // 3x for BACKSTAGE
} else if (balanceOf(user, VIP_PASS) > 0) {
return 2; // 2x for VIP
} else if (balanceOf(user, GENERAL_PASS) > 0) {
return 1; // 1x for GENERAL
}
return 0; // No pass
}

For example, once a user owns just 1 backstage pass, they permanently receive the 3x multiplier for all future performances, regardless of how many other passes they own or how many times they attend performances.

Impact:

Users can game the system by purchasing just one backstage pass to unlock permanent 3x multipliers

Proof of Concept:

function test_MultiplierBugVulnerability() public {
// Step 1: Create some performances for testing
uint256 baseReward = 100e18;
vm.startPrank(organizer);
uint256 performanceId1 = festivalPass.createPerformance(
block.timestamp + 1 hours, // Start in 1 hour
10 hours, // Duration
baseReward // Base reward: 100 BEAT getTokenBalances
);
uint256 performanceId2 = festivalPass.createPerformance(
block.timestamp + 1 hours, // Start in 1 hour
10 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
uint256 performanceId3 = festivalPass.createPerformance(
block.timestamp + 1 hours, // Start in 1 hour
10 hours, // Duration
baseReward // Base reward: 100 BEAT tokens
);
vm.stopPrank();
// Step 2: User buys 1 backstage pass (just to get the 3x multiplier) and 2 general passes
vm.startPrank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3); // 1 Backstage pass
vm.stopPrank();
// Step 3: VULNERABILITY - Check multiplier logic
// The bug: getMultiplier() only checks if user has ANY backstage pass
// It doesn't consider the quantity or other pass types
uint256 multiplier = festivalPass.getMultiplier(user1);
assertEq(multiplier, 3, "User gets 3x multiplier with just 1 backstage pass");
// Step 4: Demonstrate the economic impact
// Fast forward to performance time
vm.warp(block.timestamp + 1 hours + 30 minutes);
// User attends all performances and gets 3x multiplier for each even though they only have 1 backstage pass
vm.startPrank(user1);
festivalPass.attendPerformance(performanceId1);
vm.warp(block.timestamp + COOLDOWN);
festivalPass.attendPerformance(performanceId2);
vm.warp(block.timestamp + COOLDOWN);
festivalPass.attendPerformance(performanceId3);
vm.stopPrank();
// Check BEAT token balance
// 3x multiplier for each of the three performances
uint256 actualPerformanceReward = baseReward * 3 * 3; // 900e18
uint256 actualTotal = BACKSTAGE_WELCOME_BONUS + actualPerformanceReward;
// "User received backstage welcome bonus + 3x multiplier reward for the three performances
assertEq(beatToken.balanceOf(user1), actualTotal);
// Step 5: Demonstrate the unfair advantage
// The user has 2 general passes and 1 backstage pass
// so they should have (1 * baseReward) + (1 * baseReward) + (3 * baseReward) = 500e18
// after using all their passes to attend the performances
uint256 fairPerformanceReward = (1 * baseReward) + (1 * baseReward) + (3 * baseReward);
uint256 fairTotal = BACKSTAGE_WELCOME_BONUS + fairPerformanceReward;
// The user is able to cheat the system just buying 1 backstage pass and forever benefitting from its 3x multiplier
assertGt(actualTotal, fairTotal);
}

Recommended Mitigation:

Track which passes have been used for specific performances

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.