Beatland Festival

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

Users can purchase multiple pass types to permanently gain 3x reward multiplier

Description

Normal behavior: Users should purchase one pass type and receive corresponding multiplier (1x General, 2x VIP, 3x Backstage).

Issue: Users can purchase all three pass types and always receive the maximum 3x multiplier. The getMultiplier() function checks pass ownership hierarchically but doesn't prevent multiple pass ownership.

Root Cause

// @> getMultiplier() always returns highest multiplier if user owns multiple passes
function getMultiplier(address user) public view returns (uint256) {
if (balanceOf(user, BACKSTAGE_PASS) > 0) {
return 3; // @> Always returns 3x if backstage owned
} 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
}

The buyPass() function has no restrictions preventing multiple purchases per user.

Risk

Likelihood:

  • Users can immediately exploit this by purchasing multiple passes

  • No special conditions or timing required - always available

Impact:

  • Economic exploitation: 1.6 ETH investment yields permanent 3x reward advantage

  • Token economics imbalance: exploiters receive 300 BEAT instead of 100 BEAT per performance

Proof of Concept

function test_PassStackingExploit() public {
vm.startPrank(attacker);
// Buy all pass types (total: 1.6 ETH)
festivalPass.buyPass{value: 0.1 ether}(1); // General
festivalPass.buyPass{value: 0.5 ether}(2); // VIP + 5 BEAT bonus
festivalPass.buyPass{value: 1 ether}(3); // Backstage + 15 BEAT bonus
// Always gets 3x multiplier
assertEq(festivalPass.getMultiplier(attacker), 3);
// Performance attendance yields 300 BEAT instead of 100 BEAT
festivalPass.attendPerformance(0);
// Result: 300 BEAT earned (100 base * 3x multiplier)
}

Recommended Mitigation

function buyPass(uint256 collectionId) external payable {
+ require(!hasPass(msg.sender), "User already owns a pass");
// Must be valid pass ID (1 or 2 or 3)
require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
// ... rest of function
}

This prevents users from purchasing multiple passes, maintaining the intended tiered reward system.

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.