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.