Beatland Festival

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

Pass Transfer Allows Multiple Attendance Rewards for a Single Performance

Description

  • The attendPerformance function is intended to reward a user for attending a specific performance. The contract attempts to prevent a user from attending the same performance twice.

  • The attendance tracking mechanism uses a mapping hasAttended[performanceId][msg.sender]. This check is based only on the caller's address (msg.sender). If a user attends a performance and then transfers their pass to another user, the new user can also attend the same performance because their address is not yet in the hasAttended mapping. This allows a single pass to claim rewards multiple times for the same event.

// src/FestivalPass.sol
mapping(uint256 => mapping(address => bool)) public hasAttended;
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");
@> 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:

  • A user buys a pass, attends a performance, and then sells or transfers the pass on a secondary market to another user before the performance ends.

Impact:

  • The protocol mints more BEAT tokens than intended, leading to inflation and devaluing the token for all holders.

  • It creates an unfair economic advantage for users who can coordinate to transfer passes during a performance window, undermining the fairness of the reward system.

Proof of Concept

function test_DoubleDippingAfterPassTransfer() public {
// 1. User1 buys a backstage pass
vm.prank(user1);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
// 2. Organizer creates a performance
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(block.timestamp + 1 hours, 2 hours, 100e18);
// 3. Warp to performance time
vm.warp(block.timestamp + 90 minutes);
// 4. User1 attends the performance
vm.prank(user1);
festivalPass.attendPerformance(perfId);
// Assert User1 received rewards (15 welcome + 100 * 3)
assertEq(beatToken.balanceOf(user1), 15e18 + 300e18);
assertTrue(festivalPass.hasAttended(perfId, user1));
// 5. User1 transfers the pass to User2
vm.prank(user1);
festivalPass.safeTransferFrom(user1, user2, 3, 1, "");
// 6. User2 now attends the *same* performance
vm.prank(user2);
festivalPass.attendPerformance(perfId);
// Assert User2 also received rewards, proving the double-dip
assertEq(beatToken.balanceOf(user2), 300e18);
assertTrue(festivalPass.hasAttended(perfId, user2));
}

Recommended Mitigation

The current attendance tracking mechanism is insufficient as it is tied to the user's address and not the pass itself. A robust solution requires a design change to link attendance to a unique identifier for each pass. Since ERC1155 treats passes of the same type as fungible, this is non-trivial.

A complete fix would likely involve a significant re-architecture, such as treating each pass as a unique NFT (ERC721) to allow for individual tracking. However, a partial mitigation can be implemented to cap the potential damage:

Updates

Lead Judging Commences

inallhonesty Lead Judge 3 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.