Beatland Festival

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

A User Can Attend All New Activated Performances with a Single Purchased Pass

A User Can Attend All New Activated Performances with a Single Purchased Pass

Description

  • After purchasing a single pass, a user can attend one performance per newly activated stage, as long as the performance is currently active.

  • After attending a performance, the system does not deduct the pass from the user’s balance.

Risk

Likelihood:

  • A user only needs to purchase one pass once, and can then attend any number of new performances as long as they are active.

Impact:

  • This significantly devalues the NFT passes, as users have no incentive to purchase multiple passes.

  • Users who have already purchased multiple passes are effectively financially disadvantaged, as they paid for more than necessary.

Proof of Concept

function test_OnePassAttendAllPerformance() public {
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
uint256 performanceCount = 10;
uint256[] memory performanceIds = new uint256[](performanceCount);
for (uint256 i = 0;i < performanceCount;i++) {
// Create many performances
vm.startPrank(organizer);
// set performance duration is enough long
uint256 perfId = festivalPass.createPerformance(block.timestamp + 1 hours, 15 hours, 100e18);
performanceIds[i] = perfId;
vm.stopPrank();
}
vm.warp(block.timestamp + 1 hours + 1);
for (uint256 i = 0;i < performanceCount;i++) {
// Attend all performance
vm.prank(user1);
festivalPass.attendPerformance(performanceIds[i]);
// Wait for cooldown and attend
vm.warp(block.timestamp + 1 hours + 1);
}
assertEq(beatToken.balanceOf(user1), performanceCount * 100e18);
}

Recommended Mitigation

  • Best Practice: When a user attends a performance, they should explicitly specify which pass they are using. After receiving the reward, the corresponding pass should be burned or deducted from their balance.

function hasPass_fix(address user, uint256 usingPassId) public view returns (bool) {
if (usingPassId == BACKSTAGE_PASS && balanceOf(user, BACKSTAGE_PASS) > 0) {
return true;
} else if (usingPassId == VIP_PASS && balanceOf(user, VIP_PASS) > 0) {
return true;
} else if (usingPassId == GENERAL_PASS && balanceOf(user, GENERAL_PASS) > 0) {
return true;
}
return false;
}
function getMultiplier_fix(address user, uint256 usingPassId) public view returns (uint256) {
if (usingPassId == BACKSTAGE_PASS && balanceOf(user, BACKSTAGE_PASS) > 0) {
return 3; // 3x for BACKSTAGE
} else if (usingPassId == VIP_PASS && balanceOf(user, VIP_PASS) > 0) {
return 2; // 2x for VIP
} else if (usingPassId == GENERAL_PASS && balanceOf(user, GENERAL_PASS) > 0) {
return 1; // 1x for GENERAL
}
return 0; // No pass
}
function attendPerformance_fix(uint256 performanceId, uint256 usingPassId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass_fix(msg.sender,usingPassId), "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_fix(msg.sender, usingPassId);
if (usingPassId == BACKSTAGE_PASS) {
_burn(msg.sender,BACKSTAGE_PASS,1);
} else if (usingPassId == VIP_PASS ) {
_burn(msg.sender,VIP_PASS,1);
} else if (usingPassId == GENERAL_PASS) {
_burn(msg.sender,GENERAL_PASS,1);
}
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 25 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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