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 {
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);
}
}
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);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(3);
vm.stopPrank();
assertEq(festivalPass.balanceOf(user1, 3), 3);
assertEq(festivalPass.getMultiplier(user1), 3);
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, "");