Missing per-address cap and purchased-mapping check in buy()
The buy() function contains no check to determine whether a given address has already purchased a pass, nor does it enforce any per-wallet quantity cap. This means any external account can call buy() an unlimited number of times within a single transaction or across multiple transactions, accumulating as many passes as the remaining supply allows.
Because Ethereum transactions are atomic and gas limits on modern networks are high enough to support hundreds of consecutive calls, a malicious actor can write a simple attack contract that loops through buy() until passSupply is exhausted. Once the supply is drained, all subsequent calls from legitimate users will revert with a "supply exhausted" error, effectively locking out the entire user base from acquiring passes.
This is a classic application-level Denial of Service (DoS) pattern. It does not require any exploit of the EVM itself — only the absence of a business-logic guard. The attacker does not even need to be sophisticated; a basic script or a simple smart contract loop is sufficient. Since the attacker acquires real tokens in the process, there is also a secondary financial motivation: they can resell passes on the secondary market at a premium after manufacturing artificial scarcity.
Likelihood:
This vulnerability is rated High likelihood. The attack requires only a standard EOA or a minimal attack contract and a sufficient ETH balance to purchase all passes. There is no time-lock, no CAPTCHA-equivalent, no rate limiting on-chain, and no whitelist preventing this. The attack can be executed in a single block. Given the financial incentive (cornering the pass supply for resale), motivated actors are likely to attempt this within moments of the contract going live.
Impact:
This vulnerability is rated High impact. A successful attack prevents the entire intended user base from participating in the pass sale, rendering the launch effectively a failure. The protocol suffers reputational damage and potential legal exposure depending on the jurisdiction. While the attacker does spend ETH to acquire the passes, the harm to all other prospective buyers is severe and immediate. If the contract has a refund or cancellation mechanism, the attacker may even be able to unwind their position, making the attack low-cost with high disruption potential.
The attack contract BulkBuyer calls buy() in a tight loop up to totalSupply times in a single transaction. Since there is no per-address mapping check, every iteration succeeds. After the transaction confirms, the attacker's wallet holds all available passes and no other buyer can acquire one. The loop runs entirely within one block, making front-running countermeasures ineffective.
The fix introduces a purchaseCount mapping that tracks how many passes each address has purchased. The require guard enforces MAX_PER_WALLET — set to 1 for a one-per-wallet model, but adjustable for other use cases. This makes it impossible for any single address to drain the supply regardless of how many times they call buy(). For additional protection, consider adding a commit-reveal scheme or merkle-proof allowlist to prevent Sybil attacks where one person controls many wallets.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.