Beatland Festival

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

Pass Transfer Reward Farming Vulnerability

Root + Impact

Description

  • The contract’s normal behavior is that a user who owns a festival pass can attend a performance once, and upon attendance, receives BEAT token rewards based on their pass type. The attendance is recorded per address, so repeated attendance by the same address is prevented during the same performance.

  • The specific issue is that the attendance and reward system tracks attendance only at the address level (hasAttended[performanceId][address]) without associating the attendance or rewards with the actual NFT pass tokens. Because passes are transferable, a single pass token can be transferred to many different addresses, each able to attend the same performance separately and collect rewards. This enables an attacker to farm rewards by passing a single pass among multiple addresses, each earning full BEAT rewards, effectively allowing unlimited inflation of BEAT tokens.

// Root cause in the codebase with @> marks to highlight the relevant section
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");
@> // The attendance tracking is done per address only, missing token ownership linkage
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:

  • The issue occurs whenever a pass token is transferred to multiple different addresses that sequentially attend the same performance, each receiving BEAT rewards.

  • The ERC1155 pass tokens are transferable without any transfer restrictions or staking, enabling easy rotation of the pass among many addresses for farming.

Impact:

  • Unlimited and repeated BEAT token rewards can be minted without requiring additional passes or payments, causing severe token inflation.

  • Damage to the festival tokenomics and reward system integrity, resulting in unfair advantages and potentially devaluing BEAT tokens.

Proof of Concept

The following PoC demonstrates how an attacker who owns a single VIP Pass token can exploit the contract’s reward system by repeatedly transferring the same token to multiple addresses to claim attendance rewards multiple times for the same performance. Each new address that receives the pass token is treated independently by the contract, allowing them to attend and earn BEAT tokens despite the pass token being the same. This behavior results in unlimited farming of rewards using just one pass, which violates the intended one-reward-per-pass-per-performance model and directly inflates the BEAT token supply.

// Assume attacker holds 1 VIP_PASS token (passId = 2)
// Attacker uses Address A to attend performanceId=0 to get rewards
festivalPassContract.connect(AddressA).attendPerformance(0); // rewards minted to A
// Attacker transfers the same VIP_PASS token to Address B
festivalPassContract.safeTransferFrom(AddressA, AddressB, 2, 1, ""); // ERC1155 transfer
// Address B attends the same performanceId=0 and collects rewards again
festivalPassContract.connect(AddressB).attendPerformance(0); // rewards minted to B
// Repeat transfers and attendances across many addresses to farm unlimited BEAT tokens

Recommended Mitigation

- require(!hasAttended[performanceId][msg.sender], "Already attended this performance");
+ // Track attendance per unique token ID owned instead of per address
+ // Pseudo-code example
+ bool attendedAnyToken = false;
+ for each passType in [GENERAL_PASS, VIP_PASS, BACKSTAGE_PASS] {
+ if(balanceOf(msg.sender, passType) > 0) {
+ // Use token-specific attendance tracking
+ for(uint tokenIndex = 0; tokenIndex < balanceOf(msg.sender, passType); tokenIndex++) {
+ if(!hasAttendedToken[performanceId][tokenId]) {
+ hasAttendedToken[performanceId][tokenId] = true;
+ attendedAnyToken = true;
+ break;
+ }
+ }
+ }
+ }
+ require(attendedAnyToken, "All tokens already attended");

Alternatively,

- // No transfer restrictions on passes
+ // Implement staking or locking of passes during performance windows to prevent transfer abuse
+ function _beforeTokenTransfer(...) internal override {
+ require(!performanceActiveDuringTransfer, "Cannot transfer passes during active performances");
+ }

Implementing either modification prevents the same pass token from being used multiple times to farm attendance rewards across different addresses.

Updates

Lead Judging Commences

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