Beatland Festival

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

`buyPass()` allows unlimited pass purchases per user, inflating BEAT supply via repeated welcome bonuses

Description

Root + Impact

  • The buyPass() function does not restrict a user from purchasing multiple passes of the same or different types. Each VIP purchase grants 5e18 BEAT and each BACKSTAGE purchase grants 15e18 BEAT as a welcome bonus.

  • A user (or attacker) can repeatedly buy BACKSTAGE passes to farm 15e18 BEAT per purchase. Since getMultiplier() only uses the highest-tier pass, all additional passes beyond the first provide no extra attendance benefit — but the BEAT bonus is granted every time.

function buyPass(uint256 collectionId) external payable {
// @> No check: does msg.sender already own a pass of this type?
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); // @> Minted on EVERY purchase, not just the first
}
}

Risk

Likelihood:

  • Any user with sufficient ETH can call buyPass() repeatedly — no restrictions exist

  • With BACKSTAGE_MAX_SUPPLY = 100, a single user could buy all 100 backstage passes

Impact:

  • A user buying all 100 backstage passes would farm 1,500e18 BEAT tokens (100 * 15e18) in welcome bonuses alone

  • This inflates the BEAT token supply and devalues BEAT for legitimate single-pass holders

  • Memorabilia collections priced in BEAT become cheaper in real terms for the attacker


Proof of Concept

function test_RepeatedPassBuyFarmsBEAT() public {
vm.deal(user1, 100 ether);
vm.startPrank(user1);
// Buy 3 backstage passes
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
vm.stopPrank();
// User has 3 passes but multiplier is still just 3x
assertEq(festivalPass.balanceOf(user1, 3), 3);
assertEq(festivalPass.getMultiplier(user1), 3);
// User farmed 45e18 BEAT from welcome bonuses (3 * 15e18)
assertEq(beatToken.balanceOf(user1), 45e18);
console.log("Farmed BEAT from repeat purchases:", beatToken.balanceOf(user1));
}

Recommended Mitigation

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");
+ require(balanceOf(msg.sender, collectionId) == 0, "Already owns this pass type");
_mint(msg.sender, collectionId, 1, "");
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days 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!