Beatland Festival

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

No Reentrancy Protection in redeemMemorabilia

Root + Impact

Description

  • The redeemMemorabilia function performs a sequence of sensitive operations including:

    • Burning BEAT tokens via an external call (BeatToken(beatToken).burnFrom)

    • Updating internal state (currentItemId++, token mapping)

    • Minting ERC1155 NFTs (_mint)

    Since these operations are not protected by a nonReentrant modifier, and because they interact with external contracts and user-controlled logic, this function is vulnerable to a reentrancy attack. If the attacker controls a contract with a fallback function that calls back into redeemMemorabilia, they may be able to mint multiple items before the currentItemId is incremented or the token limit is enforced.

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood

  • burnFrom is an external call that may not be hardened.

  • _mint triggers ERC1155 onERC1155Received, which is potentially reentrant.

  • No reentrancy guard or call ordering protections.

Impact

  • Minting more memorabilia than allowed.

  • Circumventing token burn cost (BEAT).

  • Supply inflation and value erosion of memorabilia NFTs.

Proof of Concept

contract Exploit {
FestivalPass fp;
bool hasReentered;
constructor(address _fp) {
fp = FestivalPass(_fp);
}
function attack(uint256 collectionId) external {
fp.redeemMemorabilia(collectionId);
}
fallback() external {
if (!hasReentered) {
hasReentered = true;
fp.redeemMemorabilia(1); // reenter before state is updated
}
}
}
//Assumes the fallback is triggered during _mint, allowing recursive entry into redeemMemorabilia.

Recommended Mitigation

- function redeemMemorabilia(uint256 collectionId) external {
+ function redeemMemorabilia(uint256 collectionId) external nonReentrant {
// Validate collection and user eligibility
require(collection.isActive, "Collection not active");
// Burn tokens BEFORE minting
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Update internal state
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
tokenIdToEdition[tokenId] = itemId;
// Perform external mint last
_mint(msg.sender, tokenId, 1, "");
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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