Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Unrestricted Access to `FestivalPass::redeemMemorabilia()` Allows Bypassing Attendee Rewards Logic

Unrestricted Access to FestivalPass::redeemMemorabilia() Allows Bypassing Attendee Rewards Logic

Description

The function FestivalPass::redeemMemorabilia() allows any address to redeem memorabilia NFTs as long as they hold enough BEAT tokens, without any verification that the caller:

  • owns a valid festival pass

  • has attended any performance (which is how BEAT tokens are normally earned).

This directly contradicts the project documentation, which states that only users who attend performances using festival passes should be able to redeem memorabilia using BEAT tokens.
As a result, any external user or bot can preemptively drain memorabilia collections by purchasing BEAT tokens off-market (e.g., via an airdrop, liquidity, or external mint) and redeem them freely.

// Redeem a memorabilia NFT from a collection
// @audit HIGH - Anyone can redeem memorabilia as long as they have enough BEAT tokens
@> 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");

Risk

Likelihood:

  • The function is external, so anyone can call it with no access restriction

Impact:

  • Users who fairly earned BEAT tokens by attending performances may no longer be able to redeem memorabilia due to limited supply.

  • The rarity and exclusivity of collectibles can be compromised.

  • The protocol's reward system becomes meaningless, and bots can front-run user actions.

Proof of Concept

Add this POC in your FestivalPass.t.sol

address public attacker;
......
attacker = makeAddr("attacker");
vm.deal(attacker, 10 ether);
......
function test_UnauthorizedMemorabiliaRedemption() public {
// Setup: create a memorabilia collection
vm.prank(organizer);
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"Hackable Merch",
"ipfs://QmHack",
42e18,
5,
true
);
// Attacker receives BEAT tokens without attending any performance
vm.prank(address(festivalPass));
beatToken.mint(attacker, 100e18);
vm.startPrank(attacker);
beatToken.approve(address(festivalPass), 100e18);
// User redeems memorabilia without owning a pass or attending
festivalPass.redeemMemorabilia(collectionId);
vm.stopPrank();
// Assertions: the NFT was minted
uint256 expectedTokenId = festivalPass.encodeTokenId(collectionId, 1);
assertEq(festivalPass.balanceOf(attacker, expectedTokenId), 1);
// And BEAT was burned
assertEq(beatToken.balanceOf(attacker), 58e18);
}

Recommended Mitigation

Add checks to ensure only legitimate attendees can redeem memorabilia

function redeemMemorabilia(uint256 collectionId) external {
+ require(hasPass(msg.sender), "Must own a pass");
+ require(lastCheckIn[msg.sender] != 0, "Must have attended a performance");
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");
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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