Beatland Festival

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

Allowing Multiple Pass Purchases Per User Without Additional Attendance Rewards `FestivalPass::buyPass`

Impact

The absence of a restriction on the number of tickets per user may lead to purchasing multiple tickets but without any additional reward when attending the performance.

Description

Users can buy more than one pass of the same or different types without any limit. But when they attend a performance, they get the reward only once based on their best pass, no matter how many passes they have.

// Buy a festival pass
function buyPass(uint256 collectionId) external payable {
require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
// @> No check here to prevent multiple passes purchase by the same user
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
// Welcome bonus given per purchase
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
// 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");
// @> User can only attend once per performance, regardless of how many passes
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);
}

Risk

Likelihood:

This will occur when a user purchases more than one pass of the same or different types, expecting to receive multiple rewards for attending a performance. However, during attendance, the contract checks only whether the user has attended before, not how many passes they own. As a result, the user receives only a single reward based on their highest pass type

Proof of Concept

function test_MultiplePassesOnlyOneAttendanceReward() public {
// 1. User buys 2 BACKSTAGE passes
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3); // first purchase
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3); // second purchase
// 2. Check user owns 2 passes
uint256 balance = festivalPass.balanceOf(user1, 3);
assertEq(balance, 2);
//Organizer Create Pormance
uint256 startTime = block.timestamp + 1 hours;
uint256 duration = 2 hours;
uint256 reward = 10;
vm.prank(organizer);
festivalPass.createPerformance(startTime, duration, reward);
vm.warp(block.timestamp + 1.5 hours);
vm.startPrank(user1);
festivalPass.attendPerformance(0);
vm.stopPrank();
// (BACKSTAGE_PRICE * 2)
uint expectedReward = (reward * (3 * 2));
uint actualReward = (reward * festivalPass.getMultiplier(user1));
assert(expectedReward > actualReward);
}

Recommended Mitigation

function buyPass(uint256 collectionId) external payable {
// Must be valid pass ID (1 or 2 or 3)
require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
+ require(balanceOf(msg.sender, collectionId) == 0, "You already own pass");
// Check payment and supply
require(msg.value == passPrice[collectionId], "Incorrect payment amount");
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
// Mint 1 pass to buyer
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
// VIP gets 5 BEAT welcome bonus BACKSTAGE gets 15 BEAT welcome bonus
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
if (bonus > 0) {
// Mint BEAT tokens to buyer
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
Updates

Lead Judging Commences

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

Support

FAQs

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