Summary
For user who buys multiple passes to attend performance with friends, only 1 pass will earn reward. Any extra passes that the user buys are found not being rewarded extra beat tokens when attending performance.
Description
In the attendPerformance function, the hasAttended only factors in the performance Id and user address without considering multiple passes that a single user has bought. This causes the reward is not granted for the extra passes that the user has paid for and attend performance together with friends.
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:
Impact:
Proof of Concept
In test/FestivalPass.t.sol, add new test as follow:
function test_audit_noExtraBeatTokenForUserWithMultiplePasses() public {
vm.startPrank(user1);
festivalPass.buyPass{value: VIP_PRICE}(2);
festivalPass.buyPass{value: VIP_PRICE}(2);
festivalPass.buyPass{value: VIP_PRICE}(2);
vm.stopPrank();
assertEq(festivalPass.balanceOf(user1, 2), 3);
assertEq(festivalPass.passSupply(2), 3);
assertEq(beatToken.balanceOf(user1), 15e18);
uint256 startTime = block.timestamp + 1 hours;
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(startTime, 2 hours, 50e18);
vm.warp(startTime + 30 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perfId);
uint256 userBeatTokenAftAttendPerformance = beatToken.balanceOf(user1);
uint256 userBeatTokenExpectedBalance = (50e18 * 2 * 3) + (5e18 * 3);
console.log("User beat token balance after attending performance: %d", userBeatTokenAftAttendPerformance);
console.log("User beat token expected balance: %d", userBeatTokenExpectedBalance);
assertTrue(festivalPass.hasAttended(perfId, user1));
assert(userBeatTokenAftAttendPerformance != userBeatTokenExpectedBalance);
}
Run the test in terminal as follow:
$ forge test --match-test test_audit_noExtraBeatTokenForUserWithMultiplePasses -vvv [12:31:19]
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.25
[⠘] Solc 0.8.25 finished in 659.45ms
Compiler run successful!
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_audit_noExtraBeatTokenForUserWithMultiplePasses() (gas: 346865)
Logs:
User beat token balance after attending performance:
115000000000000000000
User beat token expected balance:
315000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.33ms (215.71µs CPU time)
Ran 1 test suite in 119.43ms (1.33ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
The test shows that the beat token balance of the user after attending performance is not tally to the expected balance that the user shall have.
Recommended Mitigation
To review the reward mechanism that considers multiple pass holders by either
reward based on pass Id
reward based on number of passes used per user/pass purchaser
or if the reward system is more towards by user basis, then during the buyPass stage, additional check to restrict only 1 pass tier per user shall be implemented.