Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Pass abuse through transfer vulnerability, leading to economic loss

Description

  • The normal behavior should be that users purchase festival passes and use them to attend performances, with each pass providing access according to its type (General, VIP, or Backstage) and being consumed or tracked to prevent abuse.

  • The attendPerformance function only verifies that a user owns a pass but does not consume, burn, or mark the specific pass as used. This allows a single pass to be transferred between multiple addresses and used indefinitely to attend performances, bypassing the intended economic model where each attendance should require a valid, unused pass.

function attendPerformance(uint256 performanceId) external {
require(isPerformanceActive(performanceId), "Performance is not active");
require(hasPass(msg.sender), "Must own a pass"); // @> Only checks ownership, doesn't consume the 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); // @> Rewards are minted without consuming the pass
emit Attended(msg.sender, performanceId, performances[performanceId].baseReward * multiplier);
}

Risk

Likelihood:

  • Users can easily transfer passes using the standard ERC1155 safeTransferFrom function after attending a performance

  • No technical barriers prevent this exploit - it only requires basic knowledge of token transfers

  • The vulnerability can be exploited repeatedly across multiple performances and multiple users

Impact:

  • Economic loss to the festival organizer as a single pass purchase generates unlimited attendance revenue through BEAT token minting

  • Unfair advantage for users who exploit this vulnerability compared to honest users who purchase individual passes

  • Undermines the entire festival pass pricing model and token economics

  • Potential for coordinated abuse where groups of users share a single pass across multiple accounts

Proof of Concept

Add the following test to the FestivalPass.t.sol file, and run it with forge test --match-contract FestivalPassTest --match-test test_AttendPerformance_MultipleUsesOfASinglePass.

In this test, we can see that both user1 and user2 attend the performance and get the reward, but only user1 have bought the pass. The same pass could be transferred indefinitely, leading to economic loss for the festival organizer.

function test_AttendPerformance_MultipleUsesOfASinglePass() public {
// Organizer creates a performance
uint256 startTime = block.timestamp + 1 hours;
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(startTime, 2 hours, 100e18);
// Warp to performance time
vm.warp(startTime + 30 minutes);
// User1 buys a single general pass
vm.startPrank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// User1 attends
festivalPass.attendPerformance(perfId);
vm.stopPrank();
assertEq(beatToken.balanceOf(user1), 100e18);
assertTrue(festivalPass.hasAttended(perfId, user1));
assertEq(festivalPass.lastCheckIn(user1), block.timestamp);
// User2 doesn't have the pass
assertEq(beatToken.balanceOf(user2), 0);
assertEq(festivalPass.balanceOf(user2, 1), 0);
// User1 transfers the pass to user2
vm.prank(user1);
festivalPass.safeTransferFrom(user1, user2, 1, 1, "");
assertEq(festivalPass.balanceOf(user2, 1), 1);
// User2 attends
vm.prank(user2);
festivalPass.attendPerformance(perfId);
assertEq(beatToken.balanceOf(user2), 100e18);
assertTrue(festivalPass.hasAttended(perfId, user2));
assertEq(festivalPass.lastCheckIn(user2), block.timestamp);
}

Recommended Mitigation

To solve this problem, the pass must be bound to the buyer's address.
One way to do this is to migrate the pass logic from the ERC1155 contract to an internal logic, for example, having a map of addresses => pass IDs,. Since the ERC1155 is designed to be transferable, thus incompatible with the FestivalPass design, it's easier to migrate the logic instead of doing various workarounds. This new logic must be able to track the pass ownership and prevent the pass from being used by other addresses. Since the ERC1155 now would only be used to mint memorabilia NFTs, it could be replaced by the ERC721 standard, leading to a more gas efficient implementation.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Unlimited beat farming by transferring passes to other addresses.

Support

FAQs

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