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
Alice buys a VIP pass and receives 5 BEAT as a bonus.
The organizer creates a performance.
Time advances into the performance period.
Alice attends the performance and receives 6 BEAT in total.
Alice transfers the pass to Bob.
Bob also attends and receives 5 BEAT.
Bob transfers the pass to Dan.
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);
festivalPass.buyPass{value: 0.1 ether}(2);
assertEq(beatToken.balanceOf(alice), 5e18);
vm.startPrank(organizer);
uint256 performanceId = festivalPass.createPerformance(1 days, 2 days, 3 ether);
vm.startPrank(alice);
vm.warp(block.timestamp + 1 days);
festivalPass.attendPerformance(performanceId);
assertEq(beatToken.balanceOf(alice), 11e18);
festivalPass.safeTransferFrom(alice, bob, 2, 1, "");
assertEq(festivalPass.balanceOf(bob, 2), 1);
vm.stopPrank();
vm.startPrank(bob);
festivalPass.attendPerformance(performanceId);
assertEq(beatToken.balanceOf(bob), 6e18);
festivalPass.safeTransferFrom(bob, dan, 2, 1, "");
assertEq(festivalPass.balanceOf(dan, 2), 1);
vm.stopPrank();
vm.startPrank(dan);
festivalPass.attendPerformance(0);
assertEq(beatToken.balanceOf(dan), 6e18);
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");
+ }