Beatland Festival

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

`FestivalPass::attendPerformance` The same pass can be used by multiple users to attend and earn BEAT tokens

FestivalPass::attendPerformance The same pass can be used by multiple users to attend and earn BEAT tokens

Description

The attendPerformance() function allows any user with a pass to attend a performance and receive BEAT tokens. Attendance is marked with hasAttended[performanceId][msg.sender] = true, limiting use by address, but not by the pass itself.

Since passes are fungible and transferable, the same pass can be used by multiple users (via quick transfer) within the active period of the performance. This enables abuse of the rewards system, bypassing cooldown and single-payment limitations.

@> 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: High

Any user can transfer a pass to another address and continue attending performances without restrictions.

Impact: High

Multiple users can benefit from a single pass, earning unlimited BEAT tokens. This creates uncontrolled inflation and discourages legitimate pass purchases.

Proof of Concept

  1. Alice buys a VIP pass and receives 5 BEAT as a bonus.

  2. The organizer creates a performance.

  3. Time advances into the performance period.

  4. Alice attends the performance and receives 6 BEAT in total.

  5. Alice transfers the pass to Bob.

  6. Bob also attends and receives 5 BEAT.

  7. Bob transfers the pass to Dan.

  8. Dan attends and receives 5 BEAT.

function test_MultipleUsersCanExploitSamePass() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
address dan = makeAddr("dan");
vm.deal(alice, 0.1 ether);
vm.prank(alice);
// 1.
festivalPass.buyPass{value: 0.1 ether}(2);
assertEq(beatToken.balanceOf(alice), 5e18);
// 2.
vm.startPrank(organizer);
uint256 performanceId = festivalPass.createPerformance(1 days, 2 days, 3 ether);
// 3.
vm.startPrank(alice);
vm.warp(block.timestamp + 1 days);
// 4.
festivalPass.attendPerformance(performanceId);
assertEq(beatToken.balanceOf(alice), 11e18);
// 5.
festivalPass.safeTransferFrom(alice, bob, 2, 1, "");
assertEq(festivalPass.balanceOf(bob, 2), 1);
vm.stopPrank();
vm.startPrank(bob);
// 6.
festivalPass.attendPerformance(performanceId);
assertEq(beatToken.balanceOf(bob), 6e18); // Bob gets 6 BEAT for attending
// 7.
festivalPass.safeTransferFrom(bob, dan, 2, 1, "");
assertEq(festivalPass.balanceOf(dan, 2), 1);
vm.stopPrank();
vm.startPrank(dan);
// 8.
festivalPass.attendPerformance(0);
assertEq(beatToken.balanceOf(dan), 6e18); // Dan gets 6 BEAT for attending
vm.stopPrank();
}
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_MultipleUsersCanExploitSamePass() (gas: 501440)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 28.82ms (4.87ms CPU time)

Recommended Mitigation

Completely disabling pass transfers via safeTransferFrom and safeBatchTransferFrom prevents a single pass from being reused by multiple users, preserving the integrity of the rewards system and the restricted access model.

+ function safeTransferFrom(
+ address /*from*/,
+ address /*to*/,
+ uint256 /*id*/,
+ uint256 /*amount*/,
+ bytes memory /*data*/
+ ) public pure override {
+ revert("Transfers are disabled");
+ }
+ function safeBatchTransferFrom(
+ address /*from*/,
+ address /*to*/,
+ uint256[] memory /*ids*/,
+ uint256[] memory /*amounts*/,
+ bytes memory /*data*/
+ ) public pure override {
+ revert("Batch transfers are disabled");
+ }
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.