Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

M-2: Performance rewards have no upper bound, enabling BEAT hyperinflation

Root + Impact

BeatToken has no supply cap, and the organizer can set any reward in createPerformance. A single check-in mints reward * multiplier to the attendee with no protocol-level limit.

Description

  • Normal behavior: attendees earn BEAT proportional to a performance’s configured baseReward and their pass tier multiplier.

  • createPerformance accepts arbitrary reward with no maximum. attendPerformance mints baseReward * multiplier directly via BeatToken.mint, diluting all holders and devaluing memorabilia priced in BEAT.

// src/FestivalPass.sol
function createPerformance(
uint256 startTime,
uint256 duration,
uint256 reward
) external onlyOrganizer returns (uint256) {
// @> No cap on reward
performances[performanceCount] = Performance({
startTime: startTime,
endTime: startTime + duration,
baseReward: reward
});
return performanceCount++;
}
function attendPerformance(uint256 performanceId) external {
// ...
uint256 multiplier = getMultiplier(msg.sender);
// @> Unchecked mint amount
BeatToken(beatToken).mint(msg.sender, performances[performanceId].baseReward * multiplier);
}
// src/BeatToken.sol — no ERC20Capped, no per-mint limit
function mint(address to, uint256 amount) external {
require(msg.sender == festivalContract, "Only_Festival_Mint");
_mint(to, amount);
}

Risk

Likelihood:

  • A compromised or careless organizer sets an extremely large reward for a performance.

  • Combined with H-1, pass rotation multiplies the already huge mint across many addresses.

Impact:

  • Total BEAT supply can jump by millions per transaction.

  • Memorabilia redemption costs (priceInBeat) become meaningless.

  • Economic design of “earn at shows, spend on merch” collapses.

Proof of Concept

function test_M02_uncappedPerformanceRewardMint() public {
uint256 hugeReward = 1_000_000e18;
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
vm.prank(organizer);
uint256 perfId = festivalPass.createPerformance(
block.timestamp + 1 hours,
2 hours,
hugeReward
);
vm.warp(block.timestamp + 90 minutes);
vm.prank(user1);
festivalPass.attendPerformance(perfId);
assertEq(beatToken.balanceOf(user1), hugeReward);
assertEq(beatToken.totalSupply(), hugeReward);
}

Recommended Mitigation

+ uint256 public constant MAX_REWARD_PER_ATTENDANCE = 10_000e18;
function createPerformance(...) external onlyOrganizer returns (uint256) {
+ require(reward <= MAX_REWARD_PER_ATTENDANCE, "Reward too high");
// ...
}

And/or inherit ERC20Capped on BeatToken and enforce a global max supply.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!