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.
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");
_mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
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);
}
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);
}
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 {
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
uint256 balance = festivalPass.balanceOf(user1, 3);
assertEq(balance, 2);
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();
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);
}