Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: low
Likelihood: low
Invalid

Zero Multiplier Silent Failure — Lost Rewards

Zero Multiplier Silent Failure — Lost Rewards

Description

  • Normal behaviour: attendPerformance() should mint BEAT tokens equal to baseReward * multiplier where multiplier reflects the caller's pass type.

  • Issue: The function checks hasPass(msg.sender) early but calculates getMultiplier(msg.sender) later. If the user transfers away their pass between these calls (e.g., in a multi-call or complex transaction), multiplier becomes 0, resulting in zero BEAT minted while still marking attendance as complete.

function attendPerformance(uint256 performanceId) external {
require(hasPass(msg.sender), "Must own a pass"); // ✅ Pass owned here
// ... other checks ...
hasAttended[performanceId][msg.sender] = true;
@> uint256 multiplier = getMultiplier(msg.sender); // Could be 0 if pass transferred
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
}

Risk

Likelihood:

  • Users making complex transactions that involve pass transfers.

  • MEV bots or smart contracts executing multiple operations atomically.

Impact:

  • User permanently loses reward opportunity for that performance (attendance marked complete).

  • Silent failure — no revert, just zero tokens minted.

Proof of Concept

function test_ZeroMultiplierSilent() public {
// Alice buys VIP pass
vm.prank(alice);
festivalPass.buyPass{value: 0.1 ether}(VIP_PASS);
// Organizer creates performance
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(block.timestamp + 1 hours, 2 hours, 100e18);
// Warp to performance time
vm.warp(block.timestamp + 90 minutes);
// Alice creates malicious contract that transfers pass mid-execution
MaliciousTransferrer malicious = new MaliciousTransferrer(address(festivalPass), alice, bob);
// Alice transfers VIP pass to malicious contract
vm.prank(alice);
festivalPass.safeTransferFrom(alice, address(malicious), VIP_PASS, 1, "");
// Malicious contract attends performance and transfers pass away mid-execution
vm.prank(address(malicious));
malicious.attackAttendance(perfId);
// Attendance marked but zero BEAT received
assertTrue(festivalPass.hasAttended(perfId, address(malicious)), "Attendance marked");
assertEq(beatToken.balanceOf(address(malicious)), 0, "Zero BEAT minted");
}
contract MaliciousTransferrer {
FestivalPass festival;
address recipient;
uint256 constant VIP_PASS = 2;
bool transferred;
constructor(address _festival, address _alice, address _recipient) {
festival = FestivalPass(_festival);
recipient = _recipient;
}
function attackAttendance(uint256 perfId) external {
festival.attendPerformance(perfId);
}
// ERC1155 receiver that transfers pass away during mint callback
function onERC1155Received(address, address, uint256, uint256, bytes calldata) external returns (bytes4) {
if (!transferred) {
transferred = true;
festival.safeTransferFrom(address(this), recipient, VIP_PASS, 1, "");
}
return this.onERC1155Received.selector;
}
}

Recommended Mitigation

function attendPerformance(uint256 performanceId) external {
require(hasPass(msg.sender), "Must own a pass");
+ uint256 multiplier = getMultiplier(msg.sender); // Calculate multiplier early
+ require(multiplier > 0, "No valid pass owned"); // Explicit check
require(!hasAttended[performanceId][msg.sender], "Already attended");
require(block.timestamp >= lastCheckIn[msg.sender] + COOLDOWN, "Cooldown 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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.