buyPass(uint256 collectionId) aims to mint one pass per call and stops when passSupply[collectionId] reaches passMaxSupply[collectionId].
The sequence, however, is interactions → effects rather than the recommended checks-effects-interactions pattern:
When msg.sender is a smart contract, _mint() triggers onERC1155Received before passSupply++ executes.
A malicious receiver can re-enter buyPass(id) inside that hook, the supply check still sees the old value and passes. The attacker can loop N times, exhausting or exceeding the entire supply in a single transaction.
Quantity breach: attacker mints arbitrary numbers of General/VIP/Backstage passes.
Economic dilution: scarcity model and pricing lose credibility.
Downstream issues: bonus BEAT minting (BeatToken.mint) also executes per re-entrant call.
Likelihood:
Requires only a custom contract wallet implementing onERC1155Received.
Impact:
Unlimited extra passes.
Organiser’s revenue & reputation harmed.
How the attack works:
EvilBuyer calls buyPass() once (function attack()).
FestivalPass transfers ONE pass to EvilBuyer.
During that transfer the ERC-1155 hook onERC1155Received is invoked.
The hook immediately re-enters buyPass() before passSupply++ has executed in the original frame.
The supply-cap require(passSupply < maxSupply) still sees the OLD value, so the second (and third, …) call succeeds.
When all re-entrant calls finish, the stack unwinds and each frame finally increments passSupply++, resulting in N passes minted while the organiser expected only 1.
Follow the checks-effects-interactions pattern or add re-entrancy guard.
Adding nonReentrant from OpenZeppelin’s ReentrancyGuard is another robust option.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.