Beatland Festival

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

[M-1] Non-pass-holders can redeem memorabilia using BEAT tokens received via transfer

Root + Impact

Description

  • redeemMemorabilia burns BEAT tokens and mints a memorabilia NFT to the caller. The intended flow is that only festival pass holders earn BEAT (via attendPerformance) and then use those tokens to redeem memorabilia. However, BeatToken is a standard ERC20, and BEAT tokens are freely transferable between any addresses.

  • redeemMemorabilia only checks that the caller has sufficient BEAT tokens — it does not verify that the caller owns a festival pass. This means any address that receives BEAT tokens from a pass holder can redeem memorabilia without ever participating in the festival.

function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
require(collection.currentItemId < collection.maxSupply, "Collection sold out");
// @> No check that msg.sender owns a festival pass
// @> Any address holding BEAT tokens can redeem
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
...
}

Risk

Likelihood:

  • BEAT tokens are standard ERC20 pass holders, who can transfer tokens to any wallet at any time.

  • A pass holder deliberately or accidentally sending BEAT to a non-holder enables redemption from that wallet.

Impact:

  • Memorabilia meant to be exclusive collector items for festival attendees can be acquired by wallets with no connection to the festival.

  • Dilutes the cultural and economic value of the memorabilia for legitimate pass holders.

Proof of Concept

The following shows a non-pass-holder receiving transferred BEAT tokens and using them to successfully redeem memorabilia without ever owning a festival pass.

address passHolder = address(0xA);
address nonHolder = address(0xB);
// passHolder attends a performance and earns BEAT tokens
vm.prank(passHolder);
festivalPass.attendPerformance(0);
// passHolder now has BEAT tokens
// passHolder transfers BEAT to nonHolder (no pass)
vm.prank(passHolder);
BeatToken(beatToken).transfer(nonHolder, collectionPrice);
// nonHolder redeems memorabilia — no pass required
vm.prank(nonHolder);
festivalPass.redeemMemorabilia(collectionId);
// succeeds — nonHolder owns memorabilia NFT without ever buying a pass

Recommended Mitigation

Add a pass-ownership check at the top of redeemMemorabilia so that only wallets holding at least one festival pass (General, VIP, or Backstage) can burn BEAT tokens for memorabilia. Implement hasPass as a helper that queries balanceOf across all three pass tier IDs.

function redeemMemorabilia(uint256 collectionId) external {
MemorabiliaCollection storage collection = collections[collectionId];
require(collection.priceInBeat > 0, "Collection does not exist");
require(collection.isActive, "Collection not active");
require(collection.currentItemId < collection.maxSupply, "Collection sold out");
+ require(hasPass(msg.sender), "Must own a festival pass to redeem memorabilia");
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 5 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!