Beatland Festival

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

Pass Transfer & Multiple Attendance Loophole

Root + Impact

Description

  • a single pass token can be used by multiple people to claim multiple rewards in one performance by rapid transfers.

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:

  • Reason 1 : This occurs when a user attends a performance, then transfers their pass to another wallet during the same event. The new wallet can also attend and claim rewards because attendance is tracked per address, not per pass.


  • Reason 2 : ERC-1155 passes are easily transferable. Users can automate pass transfers across multiple wallets to farm BEAT tokens from the same performance using a single pass.


Impact:

  • Impact 1 : Multiple wallets can farm BEAT tokens from a single pass, inflating token supply and breaking the reward logic.


  • Impact 2 : Undermines fairness — honest users get less value while malicious actors gain an unfair advantage, potentially devaluing the BEAT token.

Proof of Concept

  • **User1 attends a performance and earns 100 BEAT tokens.**They buy a General pass (ID 1), attend a performance (perfId), and their address is marked as having attended. So far, everything works as expected.

  • User1 transfers the same pass to User2, who also attends and earns 100 BEAT.

    Since hasAttended[perfId][user2] is still false (tracking is by address, not token), User2 is able to attend the same performance using the same pass and also earn 100 BEAT.

function test_AttendPerformance_Transfer_nft() public {
// Setup and first attendance
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(block.timestamp + 1 hours, 2 hours, 100e18);
vm.warp(block.timestamp + 90 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perfId);
console.log("user1",user1);
console.log("user2",user2);
console.log("festivalPass",address(festivalPass));
// check user1 balance
// // approve for user2
vm.prank(user1);
festivalPass.setApprovalForAll(user2, true);
// safeTransferFrom to user2
vm.prank(user1);
festivalPass.safeTransferFrom(user1,user2,1,1,"");
// // Try to attend again
vm.prank(user2);
festivalPass.attendPerformance(perfId);
assertEq(beatToken.balanceOf(user2), 100e18);
}

Recommended Mitigation

  1. Attendance is tracked per token, not per address.

    By using hasPassTokenAttended[performanceId][passTokenId], the contract ensures that each pass token ID can only be used once per performance, regardless of who holds it.

  2. Prevents reward farming via transfers.

    Even if the pass is transferred to another wallet, the new holder cannot attend the same performance again if the token has already been used — closing the loophole.


- remove this code
+
mapping(uint256 => mapping(uint256 => bool)) public hasPassTokenAttended; // performanceId => tokenId => used
function attendPerformance(uint256 performanceId, uint256 passTokenId) external {
require(!hasPassTokenAttended[performanceId][passTokenId], "Pass already used");
hasPassTokenAttended[performanceId][passTokenId] = true;
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.