Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

[H-03] Missing initialization window allows free minting

Missing initialization window allows free minting

Description

  • The contract relies on the configurePass function to set the critical parameters (price and passMaxSupply) for ticket sales. However, the buyPass function is external and callable immediately upon contract deployment.

  • There is a temporal gap between the moment the contract is deployed and the moment the organizer successfully executes the configurePass transaction. During this window, the passPrice mapping returns the default value of 0.

  • If passMaxSupply is initialized in the constructor (or set to a default non-zero value), malicious users can front-run the organizer's configuration transaction to mint passes for free.

// Buy a festival pass
// @> Root Cause: No check if the sale has started or if price is configured
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
// @> If passPrice[collectionId] is 0 (default), this requires 0 ETH
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];
// 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:

  • High. Front-running bots and MEV searchers constantly monitor the mempool for such opportunities. Even if the organizer submits the configurePass transaction in the same block as deployment, a bot can pay a higher gas tip to insert their buyPass transaction before the configuration.

Impact:

  • Loss of Revenue. Passes intended to be sold for ETH are given away for free.

  • Economic Damage. The floor price of the collection is immediately crashed by users who obtained the asset at zero cost.

Proof of Concept

function testFrontRunConfiguration() public {
// 1. Contract is deployed (fresh state)
// Assume constructor sets maxSupply to 100, but Price is pending configuration.
// 2. Attacker notices configuration hasn't happened yet.
vm.prank(attacker);
// 3. Attacker calls buyPass with 0 value
ticketContract.buyPass{value: 0}(GENERAL_PASS);
// 4. Verification: Attacker stole a pass for free
assertEq(ticketContract.balanceOf(attacker, GENERAL_PASS), 1);
// 5. Organizer finally configures the price
vm.prank(organizer);
ticketContract.configurePass(GENERAL_PASS, 1 ether, 100);
}

Assumption: The passMaxSupply is initialized to a non-zero value in the constructor (or elsewhere), making the mint technically possible.

  • Deployment: Organizer deploys the FestivalPass.

  • The Gap: The contract is now live on-chain. passPrice[ID] is 0.

Attack:

  • Attacker observes the deployment.

  • Attacker calls buyPass(ID) sending 0 ETH.

  • The check msg.value == passPrice passes (0 == 0).

  • Attacker receives the NFT.

Too Late: Organizer's transaction configurePass(ID, 1 ether, ...) confirms after the attacker's transaction.

Recommended Mitigation

  • Constructor Initialization: Set the initial price and supply immediately in the constructor so there is never a gap where the state is uninitialized

constructor(address _beatToken, address _organizer) ERC1155("ipfs://beatdrop/{id}") Ownable(msg.sender){
setOrganizer(_organizer);
beatToken = _beatToken;
+ // Initialize default values immediately
+ passPrice[GENERAL_PASS] = 0.1 ether;
+ passMaxSupply[GENERAL_PASS] = 100;
}
  • Pause Mechanism (Recommended): Use a paused boolean (or OpenZeppelin Pausable). The contract should deploy in a paused state, preventing any mints until the organizer explicitly unpauses it after configuration is complete.

+ bool public saleActive = false;
function buyPass(uint256 collectionId) external payable {
+ require(saleActive, "Sale is not active");
// ...
}
+ function setSaleStatus(bool _status) external onlyOrganizer {
+ saleActive = _status;
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!