Cover photo

Tests: InfiniteGame2 & InfiniteTreasuryManager

0x72F8...89Ff

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import "forge-std/Test.sol";
import "../src/InfiniteGame.sol";
import "../src/InfiniteGame2.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import {InfiniteToken} from "../src/InfiniteToken.sol";

uint256 constant TOTAL_SUPPLY = 1_000_000_000 ether;

// Contract for testing reentrancy protection
contract MaliciousToken is ERC20 {
    InfiniteGame2 private game2;

    constructor(address _game2) ERC20("Evil Token", "EVIL") {
        game2 = InfiniteGame2(_game2);
        _mint(address(this), TOTAL_SUPPLY);
    }

    function attack() external {
        approve(address(game2), type(uint256).max);
        game2.play();
    }

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public override returns (bool) {
        if (recipient == address(game2)) {
            game2.play(); // Attempt reentrancy
        }
        return super.transferFrom(sender, recipient, amount);
    }
}

contract InfiniteGame2Test is Test {
    InfiniteGame public game1;
    InfiniteGame2 public game2;
    InfiniteToken public gameToken;
    address public owner;
    address public player1;
    address public player2;

    function setUp() public {
        owner = address(0x42069);
        player1 = address(0x456);
        player2 = address(0x789);

        // Deploy mock token and game contract
        vm.startPrank(owner);

        gameToken = deployToken();

        game1 = new InfiniteGame(
            address(gameToken),
            block.timestamp,
            address(0)
        );
        game2 = new InfiniteGame2(
            address(gameToken),
            address(game1),
            block.timestamp
        );

        // Give tokens to test players
        gameToken.transfer(player1, 100_000_000 ether);
        gameToken.transfer(player2, 100_000_000 ether);
        vm.stopPrank();
    }

    function deployToken() public returns (InfiniteToken) {
        return new InfiniteToken(owner);
    }

    function test_InitialState() public view {
        assertEq(address(game2.gameToken()), address(gameToken));
        assertEq(address(game2.game1()), address(game1));
        assertEq(game2.startTime(), block.timestamp);
        assertEq(game2.roundStartTime(), block.timestamp);
        assertEq(game2.roundDeadline(), 0);
        assertEq(game2.lastPlayedAt(), 0);
        assertEq(game2.currentPrize(), 0);
        assertEq(game2.roundPlayCount(), 0);
        assertEq(game2.totalPlayCount(), 0);
        assertEq(game2.finalizedRounds(), 0);
        assertEq(game2.grandTotalFees(), 0);
        assertEq(game2.playerTotalFees(player1), 0);
        assertEq(game2.playerTotalFees(player2), 0);
        assertEq(game2.latestPlayer(), address(0));
    }

    function test_PlayerPlayFees() public {
        uint256 fee1 = game2.playFee();

        vm.startPrank(player1);
        gameToken.approve(address(game2), type(uint256).max);

        // First play
        game2.play();

        uint256 playerPlayFees = game2.playerTotalFees(player1);

        assertEq(playerPlayFees, fee1);
        assertEq(game2.grandTotalFees(), fee1);

        uint256 fee2 = game2.playFee();

        // Second play
        game2.play();

        playerPlayFees = game2.playerTotalFees(player1);
        assertEq(playerPlayFees, fee1 + fee2);
        assertEq(game2.grandTotalFees(), fee1 + fee2);
        vm.stopPrank();
    }

    function test_FeeIncreasesAccurately() public {
        vm.prank(owner);

        uint256 testReferenceFee = game2.playFee();

        for (uint i = 0; i < 200; i++) {
            vm.startPrank(player1);
            gameToken.approve(address(game2), game2.playFee());
            game2.play();
            vm.stopPrank();

            uint256 currentFee = game2.playFee();

            testReferenceFee = (testReferenceFee * 105) / 100;

            // Fee should increase 5% each play
            assertEq(currentFee, testReferenceFee);
        }
    }

    function test_CompleteRound() public {
        // Fund game with arbitrary amount
        vm.prank(owner);
        gameToken.transfer(address(game2), 1000 ether);

        // Player 1 plays
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        uint256 initContractBalance = gameToken.balanceOf(address(game2));
        uint256 initPlayer1Balance = gameToken.balanceOf(player1);

        // Fast forward past deadline
        vm.warp(block.timestamp + game2.countdown());

        // Finalize round
        game2.completeRound();

        // Check prize distribution
        uint256 expectedWinnerPrize = (initContractBalance *
            game2.winnerShare()) / 100;
        uint256 expectedRetained = initContractBalance - expectedWinnerPrize;

        // Verify distributions
        assertEq(
            gameToken.balanceOf(player1),
            initPlayer1Balance + expectedWinnerPrize,
            "Winner prize incorrect"
        );
        assertEq(
            gameToken.balanceOf(address(game2)),
            expectedRetained,
            "Retained amount incorrect"
        );

        assertEq(game2.roundPlayCount(), 0, "Play count not reset");
        assertEq(game2.totalPlayCount(), 1, "Total play count not incremented");
    }

    function test_RoundCompletedEventEmission() public {
        // Fund the contract
        vm.prank(owner);
        gameToken.transfer(address(game2), 1000 ether);

        // Player plays
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        uint256 gameBalance = gameToken.balanceOf(address(game2));

        // Calculate expected amounts
        uint256 expectedWinnerPrize = (gameBalance * game2.winnerShare()) / 100;
        uint256 expectedRetained = gameBalance - expectedWinnerPrize;

        // Move to round end
        vm.warp(block.timestamp + game2.countdown());

        // Expect RoundComplete event with correct parameters
        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.RoundCompleted(
            1,
            player1,
            expectedWinnerPrize,
            expectedRetained
        );
        game2.completeRound();
    }

    function test_FinalizedRoundsInitializedUsingGame1() public {
        // play a round on game 1
        vm.startPrank(player1);
        gameToken.approve(address(game1), game1.playFee());
        game1.play();
        vm.stopPrank();

        // Fast forward past deadline
        vm.warp(block.timestamp + game1.countDown());
        game1.completeRound();

        // Verify finalized rounds is 1
        assertEq(game1.finalizedRounds(), 1);

        // Play and complete a round on game 2
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();

        uint256 gameBalance = gameToken.balanceOf(address(game2));
        uint256 expectedWinnerPrize = (gameBalance * game2.winnerShare()) / 100;
        uint256 expectedRetained = gameBalance - expectedWinnerPrize;

        vm.warp(block.timestamp + game2.countdown());

        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.RoundCompleted(
            2,
            player1,
            expectedWinnerPrize,
            expectedRetained
        );

        game2.completeRound();

        // Verify finalized rounds is 2
        assertEq(game2.finalizedRounds(), 2);
    }

    function test_CompleteRound_RevertIfRoundNotOver() public {
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        vm.warp(block.timestamp + game2.countdown() - 1);
        vm.expectRevert(IInfiniteGame2.RoundStillActive.selector);
        game2.completeRound();
    }

    function test_MultipleRounds() public {
        vm.startPrank(owner);

        // Fund game with substantial amount for prizes
        gameToken.transfer(address(game2), 1000 ether);
        vm.stopPrank();

        uint256 player1initBalance = gameToken.balanceOf(player1);
        uint256 playFee1 = game2.playFee();

        // Round 1
        vm.startPrank(player1);
        gameToken.approve(address(game2), 1000 ether);
        game2.play();
        vm.stopPrank();

        vm.warp(block.timestamp + game2.countdown());
        game2.completeRound();

        // Verify Round 1 completion
        uint256 player1NewBalance = gameToken.balanceOf(player1);
        assertTrue(
            player1NewBalance > player1initBalance - playFee1,
            "Player 1 should profit from winning"
        );
        assertEq(game2.grandTotalFees(), playFee1);
        assertEq(game2.playerTotalFees(player1), playFee1);

        uint256 player2initBalance = gameToken.balanceOf(player2);

        uint256 playFee2 = game2.playFee();

        vm.startPrank(player2);
        gameToken.approve(address(game2), 1000 ether);
        game2.play();
        vm.stopPrank();

        vm.warp(block.timestamp + game2.countdown());
        game2.completeRound();

        uint256 player2NewBalance = gameToken.balanceOf(player2);

        // Player 2's final balance should be greater than their init balance minus play fee
        assertTrue(
            player2NewBalance > player2initBalance - playFee2,
            "Player 2 should profit from winning"
        );
        assertEq(game2.grandTotalFees(), playFee1 + playFee2);
        assertEq(game2.totalPlayCount(), 2);
    }

    function test_Play_RevertIfRoundNotCompleted() public {
        // Round 1
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Player wins game
        vm.warp(block.timestamp + game2.countdown());

        // Player 2 attempts play
        vm.startPrank(player2);
        gameToken.approve(address(game2), game2.playFee());
        vm.expectRevert(IInfiniteGame2.NextRoundPending.selector);
        game2.play();
        vm.stopPrank();
    }

    function test_ArbitraryTimeBeforeCompleting() public {
        // Round 1
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Player wins game
        vm.warp(block.timestamp + 42069 days);

        // Simulate player 2 playing
        vm.startPrank(player2);
        gameToken.approve(address(game2), game2.playFee());
        // Should revert
        vm.expectRevert(IInfiniteGame2.NextRoundPending.selector);
        game2.play();
        vm.stopPrank();

        uint256 player1Balance = gameToken.balanceOf(player1);

        uint256 gameBalanceBeforePayout = gameToken.balanceOf(address(game2));

        game2.completeRound();

        // Verify player 1 received prize
        assertEq(
            gameToken.balanceOf(player1),
            player1Balance +
                (gameBalanceBeforePayout * game2.winnerShare()) /
                100,
            "Player 1 should receive prize"
        );
    }

    function test_Play_RevertIfNoBalance() public {
        // Player 2 attempts play
        vm.startPrank(address(2397092));
        gameToken.approve(address(game2), game2.playFee());
        vm.expectRevert();
        game2.play();
        vm.stopPrank();
    }

    function test_Play_RevertIfBeforeStartTime() public {
        InfiniteGame2 newGame2 = new InfiniteGame2(
            address(gameToken),
            address(game1),
            block.timestamp + 10
        );

        // Give player some tokens
        vm.prank(owner);
        gameToken.transfer(player1, 1000);

        // Player 1 attempts play
        vm.startPrank(player1);
        gameToken.approve(address(newGame2), newGame2.playFee());
        vm.expectRevert(IInfiniteGame2.GameNotStarted.selector);
        newGame2.play();
        vm.stopPrank();
    }

    function test_WinnerWithdrawERC20_RevertIfNotOwner() public {
        InfiniteToken extraToken = deployToken();

        vm.prank(owner);
        extraToken.transfer(address(game2), 1000);

        vm.prank(player2);
        vm.expectRevert();
        game2.withdrawERC20(address(extraToken));
    }

    function test_WithdrawERC20_RevertIfGameToken() public {
        vm.startPrank(owner);

        // Try to withdraw game token
        vm.expectRevert(IInfiniteGame2.CannotWithdrawGameToken.selector);
        game2.withdrawERC20(address(gameToken));
        vm.stopPrank();
    }

    function test_WithdrawERC20_RevertIfNoBalance() public {
        InfiniteToken extraToken = deployToken();

        vm.prank(owner);
        vm.expectRevert(IInfiniteGame2.NoTokenBalance.selector);
        game2.withdrawERC20(address(extraToken));
    }

    function test_ReentrancyProtection() public {
        // Deploy malicious token that attempts reentrancy
        MaliciousToken malToken = new MaliciousToken(address(game2));

        vm.prank(owner);

        // Try to reenter during play
        vm.expectRevert();
        malToken.attack();
    }

    function test_PrizeCalculationRounding() public {
        // Send an amount that could cause rounding issues
        vm.prank(owner);
        gameToken.transfer(address(game2), 100);

        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        vm.warp(block.timestamp + game2.countdown());

        uint256 balanceBefore = gameToken.balanceOf(address(game2));
        game2.completeRound();
        uint256 balanceAfter = gameToken.balanceOf(address(game2));

        assertTrue(
            balanceAfter <= balanceBefore,
            "Prize distribution should not create tokens"
        );
    }

    function test_ConsecutiveRoundsWithSamePlayer() public {
        // First round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        vm.warp(block.timestamp + game2.countdown());
        game2.completeRound();

        // Same player tries next round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        assertTrue(
            game2.latestPlayer() == player1,
            "Same player should be able to play consecutive rounds"
        );
    }

    function test_EmergencyTokenRecovery() public {
        vm.startPrank(owner);
        InfiniteToken randomToken = deployToken();

        // Test recovery of accidentally sent tokens
        uint256 ownerBalanceBefore = randomToken.balanceOf(owner);

        randomToken.transfer(address(game2), 1000);

        // Owner should be able to recover random tokens
        game2.withdrawERC20(address(randomToken));

        vm.stopPrank();

        assertEq(
            randomToken.balanceOf(owner),
            ownerBalanceBefore,
            "Owner should receive recovered tokens"
        );
    }

    function test_PlayedEventEmission() public {
        vm.prank(owner);

        uint256 playFee = game2.playFee();

        vm.startPrank(player1);
        gameToken.approve(address(game2), playFee);

        vm.expectEmit(true, false, false, true, address(game2));
        emit IInfiniteGame2.Played(player1, playFee, 1);
        game2.play();
        vm.stopPrank();
    }

    // Tests the game won't brick if nobody plays at the start of a round
    function test_CompleteRound_NoPlayers() public {
        // Assert this is the first round
        assertEq(game2.finalizedRounds(), 0);

        // Warp to end of round + arbitrary time to simulate being able to continue the game
        // no matter now long nobody has played
        vm.warp(game2.roundDeadline() + 1000 weeks);

        assertEq(game2.latestPlayer(), address(0));

        // Can't complete the round until someone plays
        vm.expectRevert(IInfiniteGame2.RoundStillActive.selector);
        game2.completeRound();

        // Player can still play even though countdown has expired
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        uint256 gameBalanceBefore = gameToken.balanceOf(address(game2));
        uint256 prizeBeforeRoundEnd = game2.currentPrize();
        uint256 player1BalanceBefore = gameToken.balanceOf(player1);

        // Still need to wait for new countdown to expire
        vm.warp(game2.roundDeadline());

        // Now round can end
        game2.completeRound();

        // Player received round prize
        assertEq(
            gameToken.balanceOf(address(game2)),
            gameBalanceBefore - prizeBeforeRoundEnd
        );
        assertApproxEqAbs(
            gameToken.balanceOf(player1),
            player1BalanceBefore + prizeBeforeRoundEnd,
            0
        );

        // New round - player should be able to play
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Verify round count
        assertEq(game2.finalizedRounds(), 1);
    }

    function test_CurrentPrize_Calculation() public {
        // Test with zero balance
        assertEq(game2.currentPrize(), 0);

        // Test with specific balance
        vm.prank(owner);
        gameToken.transfer(address(game2), 1000 ether);

        // Safely calculate expected prize: (balance * winnerShare) / 100
        uint256 balance = gameToken.balanceOf(address(game2));
        uint256 expectedPrize = (balance / 100) * game2.winnerShare();

        assertEq(
            game2.currentPrize(),
            expectedPrize,
            "Prize calculation incorrect"
        );
    }

    function test_Play_MultiplePlayers() public {
        // First player
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        assertEq(game2.latestPlayer(), player1);

        // Second player
        vm.startPrank(player2);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        assertEq(game2.latestPlayer(), player2);
        assertEq(game2.roundPlayCount(), 2);
        assertEq(game2.totalPlayCount(), 2);
    }

    function test_Play_ApprovalRequired() public {
        vm.startPrank(player1);

        // Try to play without approving tokens
        vm.expectRevert();
        game2.play();

        // Approve less than required
        gameToken.approve(address(game2), game2.playFee() - 1);
        vm.expectRevert();
        game2.play();

        vm.stopPrank();
    }

    function test_SetCountdown_DuringActiveRound() public {
        // Start a round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Should not update until round completes
        vm.prank(owner);
        game2.setCountdown(2 hours);
        assertEq(game2.countdown(), 1 hours);

        // Complete round and verify update
        vm.warp(block.timestamp + game2.countdown());

        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.CountdownUpdated(2 hours);

        game2.completeRound();
        assertEq(game2.countdown(), 2 hours);
    }

    function test_SetCountdown_RevertIfInvalid() public {
        vm.startPrank(owner);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setCountdown(59); // Must be at least 60 seconds

        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setCountdown(86401); // 1 day is the max
        vm.stopPrank();
    }

    function test_SetInterestRate_DuringActiveRound() public {
        // Start a round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Should not update until round completes
        vm.prank(owner);
        game2.setInterestRate(1100);
        assertEq(game2.interestRate(), 1050);

        // Complete round and verify update
        vm.warp(block.timestamp + game2.countdown());

        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.InterestRateUpdated(1100);

        game2.completeRound();
        assertEq(game2.interestRate(), 1100);
    }

    function test_SetInterestRate_RevertIfInvalid() public {
        vm.startPrank(owner);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setInterestRate(0);

        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setInterestRate(1501);

        vm.stopPrank();
    }

    function test_SetWinnerShare_DuringActiveRound() public {
        // Start a round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Should not update until round completes
        vm.prank(owner);
        game2.setWinnerShare(60);
        assertEq(game2.winnerShare(), 50);

        // Complete round and verify update
        vm.warp(block.timestamp + game2.countdown());

        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.WinnerShareUpdated(60);
        game2.completeRound();
        assertEq(game2.winnerShare(), 60);
    }

    function test_SetWinnerShare_RevertIfInvalid() public {
        vm.startPrank(owner);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setWinnerShare(0);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setWinnerShare(100);
        vm.stopPrank();
    }

    function test_SetInitialPlayFee_DuringActiveRound() public {
        // Start a round
        vm.startPrank(player1);
        gameToken.approve(address(game2), game2.playFee());
        game2.play();
        vm.stopPrank();

        // Should not update until round completes
        vm.prank(owner);
        game2.setInitialPlayFee(2000);
        assertEq(game2.initialPlayFee(), 100 ether);

        // Complete round and verify update
        vm.warp(block.timestamp + game2.countdown());

        vm.expectEmit(true, true, true, true, address(game2));
        emit IInfiniteGame2.InitialPlayFeeUpdated(2000);

        game2.completeRound();

        assertEq(game2.initialPlayFee(), 2000);
    }

    function test_SetInitialPlayFee_RevertIfInvalid() public {
        vm.startPrank(owner);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setInitialPlayFee(0);
        vm.expectRevert(IInfiniteGame2.InvalidValue.selector);
        game2.setInitialPlayFee(100_001);
        vm.stopPrank();
    }

    receive() external payable {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/InfiniteTreasuryManager.sol";
import "./mocks/MockNonfungiblePositionManager.sol";

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MockInfiniteGameV1 {
    mapping(address => uint256) private _playerPlays;
    uint256 private _totalPlays;

    function setPlayerCount(address player, uint256 plays) external {
        _playerPlays[player] = plays;
    }

    function setTotalPlayCount(uint256 plays) external {
        _totalPlays = plays;
    }

    function playerPlayCount(address player) external view returns (uint256) {
        return _playerPlays[player];
    }

    function totalPlayCount() external view returns (uint256) {
        return _totalPlays;
    }
}

contract MockInfiniteGame2 {
    mapping(address => uint256) private _playerFees;
    uint256 private _totalFees;
    address private _owner;

    constructor() {
        _owner = msg.sender;
    }

    function setPlayerFees(address player, uint256 fees) external {
        _playerFees[player] = fees;
    }

    function setTotalFees(uint256 fees) external {
        _totalFees = fees;
    }

    function playerTotalFees(address player) external view returns (uint256) {
        return _playerFees[player];
    }

    function grandTotalFees() external view returns (uint256) {
        return _totalFees;
    }

    function owner() external view returns (address) {
        return _owner;
    }

    function setOwner(address newOwner) external {
        _owner = newOwner;
    }
}

contract MockTreasury {
    // For the sake of simplicity, we're not including the donation
    // that happens here in the real contract
    function withdraw(address token, address to, uint256 amount) external {
        MockERC20(token).transfer(to, amount);
    }

    function collectFees(uint256 tokenId) external {}
}

contract MockERC721 {
    mapping(uint256 => address) private _owners;

    function mint(address to, uint256 tokenId) external {
        _owners[tokenId] = to;
    }

    function ownerOf(uint256 tokenId) external view returns (address) {
        return _owners[tokenId];
    }

    function transferFrom(address from, address to, uint256 tokenId) external {
        require(_owners[tokenId] == from, "Not owner");
        _owners[tokenId] = to;
    }
}

contract InfiniteTreasuryManagerTest is Test {
    MockERC20 public usdc;
    MockERC20 public dai;
    MockERC20 public weth;
    MockERC20 public infiniteToken;
    MockInfiniteGameV1 public gameV1;
    MockInfiniteGame2 public gameV2;
    MockTreasury public treasury;
    InfiniteTreasuryManager public manager;
    MockNonfungiblePositionManager public positionManager;

    address public constant ZERO_ADDRESS = address(0);
    uint256 public constant POSITION_ID = 1;

    mapping(address => uint256) private playerPlayCounts;

    function setUp() public {
        usdc = new MockERC20();
        dai = new MockERC20();
        weth = new MockERC20();
        infiniteToken = new MockERC20();
        gameV1 = new MockInfiniteGameV1();
        gameV2 = new MockInfiniteGame2();
        treasury = new MockTreasury();
        positionManager = new MockNonfungiblePositionManager();

        uint256 infiniteBalance = 5313 ether;
        uint256 wethBalance = 459 ether;

        // Create position
        positionManager.setPosition(
            POSITION_ID,
            address(infiniteToken),
            address(weth),
            infiniteBalance,
            wethBalance
        );

        positionManager.setOwner(POSITION_ID, address(treasury));

        manager = new InfiniteTreasuryManager(
            address(treasury),
            address(positionManager),
            address(gameV1),
            address(gameV2),
            address(infiniteToken)
        );
    }

    function test_Constructor_InitialState() public view {
        assertEq(manager.infiniteTreasury(), address(treasury));
        assertEq(manager.infiniteGame1(), address(gameV1));
        assertEq(manager.infiniteGame2(), address(gameV2));
        assertEq(manager.infiniteToken(), address(infiniteToken));
    }

    function test_Constructor_RevertZeroAddress() public {
        vm.expectRevert(
            abi.encodeWithSelector(InfiniteTreasuryManager.ZeroAddress.selector)
        );
        new InfiniteTreasuryManager(
            ZERO_ADDRESS,
            address(positionManager),
            address(gameV1),
            address(gameV2),
            address(infiniteToken)
        );

        vm.expectRevert(
            abi.encodeWithSelector(InfiniteTreasuryManager.ZeroAddress.selector)
        );
        new InfiniteTreasuryManager(
            address(treasury),
            ZERO_ADDRESS,
            address(gameV1),
            address(gameV2),
            address(infiniteToken)
        );

        vm.expectRevert(
            abi.encodeWithSelector(InfiniteTreasuryManager.ZeroAddress.selector)
        );
        new InfiniteTreasuryManager(
            address(treasury),
            address(positionManager),
            address(gameV1),
            address(gameV2),
            ZERO_ADDRESS
        );
    }

    function test_playerWithdrawals_MultipleCollections() public {
        // Test with 3 players
        address player1 = address(1);
        address player2 = address(2);
        address player3 = address(3);

        // Set up V1 play counts
        uint256 totalPlays = 100;
        gameV1.setTotalPlayCount(totalPlays);
        gameV1.setPlayerCount(player1, 20); // 20% of plays
        gameV1.setPlayerCount(player2, 30); // 30% of plays
        gameV1.setPlayerCount(player3, 50); // 50% of plays

        // Set up V2 fees
        uint256 totalFees = 1000 ether;
        gameV2.setTotalFees(totalFees);
        gameV2.setPlayerFees(player1, 500 ether); // 50% of fees
        gameV2.setPlayerFees(player2, 300 ether); // 30% of fees
        gameV2.setPlayerFees(player3, 200 ether); // 20% of fees

        // First collection - should go to V1
        uint256 firstCollection = 1000 ether;
        weth.transfer(address(treasury), firstCollection);
        manager.collectAndDistribute(POSITION_ID);

        // Second collection - should go to V2
        uint256 secondCollection = 2000 ether;
        weth.transfer(address(treasury), secondCollection);
        manager.collectAndDistribute(POSITION_ID);

        // Test player1 withdrawable
        // Expected: Should be (20% of 1000 ETH) + (50% of 2000 ETH) = 200 + 1000 = 1200 ETH
        // Current test expectation: (20% of 1000 ETH + 50% of 2000 ETH) / 2 = 600 ETH
        uint256 player1Withdrawable = manager.withdrawable(
            player1,
            address(weth)
        );

        // The actual value from the contract should be 1200 ETH
        assertEq(
            player1Withdrawable,
            1200 ether,
            "Player 1 withdrawable incorrect"
        );

        // Test player2 withdrawable
        // Expected: Should be (30% of 1000 ETH) + (30% of 2000 ETH) = 300 + 600 = 900 ETH
        // Current test expectation: (30% of 1000 ETH + 30% of 2000 ETH) / 2 = 450 ETH
        uint256 player2Withdrawable = manager.withdrawable(
            player2,
            address(weth)
        );

        // The actual value from the contract should be 900 ETH
        assertEq(
            player2Withdrawable,
            900 ether,
            "Player 2 withdrawable incorrect"
        );

        // Test player3 withdrawable
        // Expected: Should be (50% of 1000 ETH) + (20% of 2000 ETH) = 500 + 400 = 900 ETH
        // Current test expectation: (50% of 1000 ETH + 20% of 2000 ETH) / 2 = 450 ETH
        uint256 player3Withdrawable = manager.withdrawable(
            player3,
            address(weth)
        );

        // The actual value from the contract should be 900 ETH
        assertEq(
            player3Withdrawable,
            900 ether,
            "Player 3 withdrawable incorrect"
        );
    }

    function test_Withdraw_RevertZeroAddress() public {
        vm.expectRevert(
            abi.encodeWithSelector(InfiniteTreasuryManager.ZeroAddress.selector)
        );
        manager.withdraw(ZERO_ADDRESS, address(weth));
    }

    function test_Withdraw_RevertInsufficientBalance() public {
        // No deposits made
        vm.expectRevert(abi.encodeWithSignature("InsufficientBalance()"));
        manager.withdraw(address(this), address(weth));
    }

    function test_Withdrawable_NoDeposits() public {
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(address(this), 50);
        gameV2.setTotalFees(100 ether);
        gameV2.setPlayerFees(address(this), 50 ether);

        assertEq(manager.withdrawable(address(this), address(weth)), 0);
    }
    function test_Withdrawable_AlreadyWithdrawn() public {
        // Set up initial state
        uint256 wethAmount = 100 ether;
        weth.transfer(address(treasury), wethAmount);

        // Set equal proportions in V1 and V2
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(address(this), 50);
        gameV2.setTotalFees(100 ether);
        gameV2.setPlayerFees(address(this), 50 ether);

        manager.collectAndDistribute(POSITION_ID);

        uint256 withdrawable = manager.withdrawable(
            address(this),
            address(weth)
        );
        assertEq(withdrawable, 50 ether); // Should be 50% in both V1 and V2, averaged to 50%

        manager.withdraw(address(this), address(weth));

        // Should be 0 after withdrawal
        assertEq(manager.withdrawable(address(this), address(weth)), 0);
    }

    function test_Withdrawable_NoV2Activity() public {
        uint256 wethAmount = 100 ether;
        weth.transfer(address(treasury), wethAmount);

        // Only V1 activity
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(address(this), 50);
        gameV2.setTotalFees(0); // No V2 activity

        manager.collectAndDistribute(POSITION_ID);

        // Should use only V1 share (50%) since V2 has no activity
        assertEq(manager.withdrawable(address(this), address(weth)), 50 ether);
    }

    function test_Withdrawable_NoV1Activity() public {
        uint256 wethAmount = 100 ether;
        weth.transfer(address(treasury), wethAmount);

        // Only V2 activity
        gameV1.setTotalPlayCount(0);
        gameV2.setTotalFees(1000 ether);
        gameV2.setPlayerFees(address(this), 500 ether);

        manager.collectAndDistribute(POSITION_ID);

        uint256 withdrawable = manager.withdrawable(
            address(this),
            address(weth)
        );

        console.log("Withdrawable: %d", withdrawable);

        // Should be zero because player didn't play V1
        assertEq(withdrawable, 0);
    }

    function test_Withdrawable_MixedActivity() public {
        uint256 v1UniswapFees = 1000 ether;
        weth.transfer(address(treasury), v1UniswapFees);

        address player1 = address(1);
        address player2 = address(2);

        // Different shares in V1 and V2
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(player1, 25); // 25% in V1
        gameV1.setPlayerCount(player2, 75); // 75% in V1

        manager.collectAndDistribute(POSITION_ID);

        // First collection should only be V1 share (25%)
        assertEq(manager.withdrawable(player1, address(weth)), 250 ether);
        assertEq(manager.withdrawable(player2, address(weth)), 750 ether);

        // Player 1 withdraws
        manager.withdraw(player1, address(weth));
        assertEq(weth.balanceOf(player1), 250 ether);

        // V2 starts after V1 fees have been collected
        uint256 v2UniswapFees = 1000 ether;
        weth.transfer(address(treasury), v2UniswapFees);

        // Set V2 playing fees
        gameV2.setTotalFees(100 ether);
        gameV2.setPlayerFees(player1, 60 ether);
        gameV2.setPlayerFees(player2, 40 ether);

        // Distribute v2 uniswap fees
        manager.collectAndDistribute(POSITION_ID);

        // Player 1 has withdrawn V1 uniswap fees, so should only get 60% of V2
        assertEq(manager.withdrawable(player1, address(weth)), 600 ether);

        // Player 2 hasn't withdrawn v2 uniswap yet, so should be 75% of V1 + 40% of V2
        assertEq(
            manager.withdrawable(player2, address(weth)),
            750 ether + 400 ether
        );
    }

    function test_Withdrawable_SmallAmounts() public {
        // Test with 1 wei to verify rounding behavior
        weth.transfer(address(treasury), 1);

        gameV1.setTotalPlayCount(3);
        gameV1.setPlayerCount(address(this), 1); // 33.33...% in V1
        gameV2.setTotalFees(3);
        gameV2.setPlayerFees(address(this), 1); // 33.33...% in V2

        manager.collectAndDistribute(POSITION_ID);

        // With floor rounding, should get 0
        assertEq(manager.withdrawable(address(this), address(weth)), 0);
    }

    // Add these tests to InfiniteTreasuryManagerTest

    function test_CollectAndDistribute_InfiniteTokenFees() public {
        // Test collecting Infinite tokens
        uint256 infiniteTokenAmount = 1000 ether;
        infiniteToken.transfer(address(treasury), infiniteTokenAmount);

        // Should transfer to V2 game
        manager.collectAndDistribute(POSITION_ID);

        assertEq(infiniteToken.balanceOf(address(gameV2)), infiniteTokenAmount);
    }

    function test_CollectAndDistribute_EmitsEvent() public {
        uint256 wethAmount = 1000 ether;
        uint256 infiniteTokenAmount = 500 ether;

        weth.transfer(address(treasury), wethAmount);
        infiniteToken.transfer(address(treasury), infiniteTokenAmount);

        vm.expectEmit(true, true, true, true);
        emit InfiniteTreasuryManager.FeesCollected(
            POSITION_ID,
            address(infiniteToken),
            address(weth),
            infiniteTokenAmount,
            wethAmount
        );

        manager.collectAndDistribute(POSITION_ID);
    }

    function test_CollectAndDistribute_ZeroFees() public {
        // Should not revert when collecting zero fees
        manager.collectAndDistribute(POSITION_ID);

        assertEq(weth.balanceOf(address(manager)), 0);
        assertEq(infiniteToken.balanceOf(address(gameV2)), 0);
    }

    function test_GetterFunctions() public view {
        assertEq(manager.infiniteTreasury(), address(treasury));
        assertEq(manager.infiniteGame1(), address(gameV1));
        assertEq(manager.infiniteToken(), address(infiniteToken));
    }

    function test_MultipleWithdrawals() public {
        uint256 wethAmount = 1000 ether;
        weth.transfer(address(treasury), wethAmount);

        address player = address(12345);

        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(player, 50);

        manager.collectAndDistribute(POSITION_ID);

        // First withdrawal
        manager.withdraw(player, address(weth));
        assertEq(weth.balanceOf(player), 500 ether);

        // Second collection
        weth.transfer(address(treasury), wethAmount);
        manager.collectAndDistribute(POSITION_ID);

        // Second withdrawal - should revert since player has no activity on V2 game
        vm.expectRevert(
            abi.encodeWithSelector(
                InfiniteTreasuryManager.InsufficientBalance.selector
            )
        );
        manager.withdraw(player, address(weth));

        // Play on V2
        gameV2.setTotalFees(1000 ether);
        gameV2.setPlayerFees(player, 500 ether);

        // Collect and distribute V2 fees
        manager.collectAndDistribute(POSITION_ID);

        // Second withdrawal should now succeed
        manager.withdraw(player, address(weth));

        assertEq(weth.balanceOf(player), 1000 ether);
    }

    function test_WithdrawableWithMaxPlayers() public {
        uint256 numPlayers = 1000; // Or higher, test gas limits
        uint256 wethAmount = 1000 ether;
        uint256 playShare = 1;

        weth.transfer(address(treasury), wethAmount);

        // Set up many players with small shares
        for (uint256 i = 1; i <= numPlayers; i++) {
            address player = address(uint160(i));
            gameV1.setPlayerCount(player, playShare);
        }
        gameV1.setTotalPlayCount(numPlayers * playShare);

        manager.collectAndDistribute(POSITION_ID);

        // Test a few random players can withdraw
        for (uint256 i = 1; i <= 10; i++) {
            address player = address(uint160(i));
            uint256 expectedAmount = wethAmount / numPlayers;
            assertEq(
                manager.withdrawable(player, address(weth)),
                expectedAmount
            );
        }
    }

    function test_WithdrawableWithPrimeAmounts() public {
        // Test with prime numbers to force recurring decimals
        uint256 wethAmount = 1000 ether;
        uint256 totalPlays = 7; // Prime number

        weth.transfer(address(treasury), wethAmount);

        gameV1.setTotalPlayCount(totalPlays);
        gameV1.setPlayerCount(address(1), 2);
        gameV1.setPlayerCount(address(2), 3);
        gameV1.setPlayerCount(address(3), 2);

        manager.collectAndDistribute(POSITION_ID);

        // Verify rounding behavior
        uint256 expected1 = (wethAmount * 2) / totalPlays;
        uint256 expected2 = (wethAmount * 3) / totalPlays;

        assertEq(manager.withdrawable(address(1), address(weth)), expected1);
        assertEq(manager.withdrawable(address(2), address(weth)), expected2);
    }

    function test_MultiplePartialWithdrawals() public {
        uint256 wethAmount = 1000 ether;
        address player = address(1);

        // Setup initial state
        weth.transfer(address(treasury), wethAmount);
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(player, 50);

        manager.collectAndDistribute(POSITION_ID);

        // First partial withdrawal
        manager.withdraw(player, address(weth));

        // Second collection
        weth.transfer(address(treasury), wethAmount);
        manager.collectAndDistribute(POSITION_ID);

        // Verify correct accounting after multiple operations
        uint256 withdrawable2 = manager.withdrawable(player, address(weth));
        assertEq(withdrawable2, 0); // Should be 0 since no V2 activity
    }

    function test_CollectAndDistribute_ArbitraryTokenPair() public {
        // Set up a new position with USDC/DAI pair
        uint256 newPositionId = 2;
        uint256 usdcAmount = 1000000 * 10 ** 6; // 1M USDC
        uint256 daiAmount = 1000000 * 10 ** 18; // 1M DAI

        positionManager.setPosition(
            newPositionId,
            address(usdc),
            address(dai),
            usdcAmount,
            daiAmount
        );
        positionManager.setOwner(newPositionId, address(treasury));

        // Transfer tokens to treasury
        usdc.transfer(address(treasury), usdcAmount);
        dai.transfer(address(treasury), daiAmount);

        // Set up V1 and V2 activity
        address player1 = address(1);
        address player2 = address(2);

        // V1: player1 = 30%, player2 = 70%
        gameV1.setTotalPlayCount(100);
        gameV1.setPlayerCount(player1, 30);
        gameV1.setPlayerCount(player2, 70);

        // Collect and distribute first batch (goes to V1)
        vm.expectEmit(true, true, true, true);
        emit InfiniteTreasuryManager.FeesCollected(
            newPositionId,
            address(usdc),
            address(dai),
            usdcAmount,
            daiAmount
        );
        manager.collectAndDistribute(newPositionId);

        // Verify V1 distribution
        assertEq(
            manager.withdrawable(player1, address(usdc)),
            (usdcAmount * 30) / 100
        );
        assertEq(
            manager.withdrawable(player1, address(dai)),
            (daiAmount * 30) / 100
        );
        assertEq(
            manager.withdrawable(player2, address(usdc)),
            (usdcAmount * 70) / 100
        );
        assertEq(
            manager.withdrawable(player2, address(dai)),
            (daiAmount * 70) / 100
        );

        // Second batch of fees
        usdc.transfer(address(treasury), usdcAmount);
        dai.transfer(address(treasury), daiAmount);

        // Set up V2 activity before second distribution
        // V2: player1 = 60%, player2 = 40%
        gameV2.setTotalFees(1000 ether);
        gameV2.setPlayerFees(player1, 600 ether);
        gameV2.setPlayerFees(player2, 400 ether);

        // Collect and distribute second batch (goes to V2)
        manager.collectAndDistribute(newPositionId);

        // Verify total withdrawable amounts
        assertEq(
            manager.withdrawable(player1, address(usdc)),
            ((usdcAmount * 30) / 100) + ((usdcAmount * 60) / 100)
        );
        assertEq(
            manager.withdrawable(player2, address(usdc)),
            ((usdcAmount * 70) / 100) + ((usdcAmount * 40) / 100)
        );
    }

    function test_EmergencyWithdrawERC20() public {
        // Transfer some tokens directly to the manager contract
        uint256 amount = 100 ether;
        address recipient = address(12345);

        // Only game owner can withdraw
        address nonOwner = address(123);
        vm.prank(nonOwner);
        vm.expectRevert(InfiniteTreasuryManager.NotGameOwner.selector);
        manager.emergencyWithdrawERC20(address(usdc), recipient);

        // Can't withdraw infinite token
        vm.prank(gameV2.owner());
        vm.expectRevert(
            InfiniteTreasuryManager.CannotWithdrawInfiniteToken.selector
        );
        manager.emergencyWithdrawERC20(address(infiniteToken), recipient);

        // Can't withdraw if no balance
        vm.prank(gameV2.owner());
        vm.expectRevert(InfiniteTreasuryManager.NoTokenBalance.selector);
        manager.emergencyWithdrawERC20(address(usdc), recipient);

        // Add balance
        usdc.transfer(address(manager), amount);

        // Successful withdrawal
        vm.prank(gameV2.owner());
        manager.emergencyWithdrawERC20(address(usdc), recipient);

        assertEq(usdc.balanceOf(address(manager)), 0);
        assertEq(usdc.balanceOf(recipient), amount);
    }

    function test_EmergencyWithdrawERC721() public {
        // Create a mock ERC721 token
        MockERC721 mockNFT = new MockERC721();
        uint256 tokenId = 123;
        address recipient = address(12345);

        // Mint NFT to manager contract
        mockNFT.mint(address(manager), tokenId);

        // Only game owner can withdraw
        address nonOwner = address(123);
        vm.prank(nonOwner);
        vm.expectRevert(InfiniteTreasuryManager.NotGameOwner.selector);
        manager.emergencyWithdrawERC721(address(mockNFT), tokenId, recipient);

        // Can't withdraw non-owned token
        uint256 nonOwnedTokenId = 456;
        vm.prank(Ownable(address(gameV2)).owner());
        vm.expectRevert(InfiniteTreasuryManager.NoTokenBalance.selector);
        manager.emergencyWithdrawERC721(
            address(mockNFT),
            nonOwnedTokenId,
            recipient
        );

        // Successful withdrawal
        vm.prank(Ownable(address(gameV2)).owner());
        manager.emergencyWithdrawERC721(address(mockNFT), tokenId, recipient);
        assertEq(mockNFT.ownerOf(tokenId), recipient);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/InfiniteTreasuryManager.sol";
import "../src/InfiniteTreasury.sol";
import "../src/InfiniteGame.sol";
import "../src/InfiniteGame2.sol";
import "./mocks/MockNonfungiblePositionManager.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";

contract MockWETH is ERC20 {
    constructor() ERC20("Wrapped Ether", "WETH") {
        _mint(msg.sender, 1e9 ether);
    }
}

contract MockInfiniteToken is ERC20 {
    constructor() ERC20("Infinite Token", "INF") {
        _mint(msg.sender, 1e9 ether);
    }
}

contract InfiniteTreasuryManagerIntegration is Test {
    InfiniteTreasuryManager public manager;
    InfiniteTreasury public treasury;
    InfiniteGame public gameV1;
    InfiniteGame2 public gameV2;
    MockWETH public weth;
    MockInfiniteToken public infiniteToken;
    MockNonfungiblePositionManager public positionManager;

    address public admin;
    address public player1;
    address public player2;

    uint256 public infiniteBalance = 5313 ether;
    uint256 public wethBalance = 459 ether;

    uint256 public constant POSITION_ID = 1;

    event FeesCollected(uint256 wethAmount, uint256 infiniteTokenAmount);

    function setUp() public {
        admin = address(this);
        player1 = address(0x1);
        player2 = address(0x2);

        // Deploy mock tokens
        weth = new MockWETH();
        infiniteToken = new MockInfiniteToken();
        positionManager = new MockNonfungiblePositionManager();

        // Deploy core contracts
        treasury = new InfiniteTreasury(address(positionManager));

        // Setup test position with accumulated fees
        positionManager.setPosition(
            POSITION_ID,
            address(infiniteToken),
            address(weth),
            infiniteBalance,
            wethBalance
        );

        infiniteToken.transfer(address(positionManager), infiniteBalance);
        weth.transfer(address(positionManager), wethBalance);

        positionManager.setOwner(POSITION_ID, address(treasury));

        gameV1 = new InfiniteGame(
            address(infiniteToken),
            block.timestamp,
            admin
        );

        gameV2 = new InfiniteGame2(
            address(infiniteToken),
            address(1),
            block.timestamp
        );

        // Deploy manager
        manager = new InfiniteTreasuryManager(
            address(treasury),
            address(positionManager),
            address(gameV1),
            address(gameV2),
            address(infiniteToken)
        );

        // Transfer treasury ownership to manager
        treasury.transferOwnership(address(manager));

        // Setup initial balances
        vm.startPrank(admin);
        infiniteToken.transfer(player1, 1_000_000 ether);
        infiniteToken.transfer(player2, 1_000_000 ether);
        weth.transfer(player1, 1_000_000 ether);
        weth.transfer(player2, 1_000_000 ether);
        vm.stopPrank();
    }

    // Helper function to simulate V1 gameplay
    function playGameV1(address player, uint256 times) internal {
        vm.startPrank(player);
        infiniteToken.approve(address(gameV1), type(uint256).max);

        for (uint256 i = 0; i < times; i++) {
            gameV1.play();
            vm.warp(block.timestamp + 1 seconds);
        }
        vm.stopPrank();
    }

    // Helper function to simulate V2 gameplay with fees
    function playGameV2(address player, uint256 times) internal {
        vm.startPrank(player);
        infiniteToken.approve(address(gameV2), type(uint256).max);
        weth.approve(address(gameV2), type(uint256).max);

        for (uint256 i = 0; i < times; i++) {
            gameV2.play();
            vm.warp(block.timestamp + 1 seconds);
        }
        vm.stopPrank();
    }

    function test_WithdrawalDistribution() public {
        // Setup plays in V1 and fees in V2
        playGameV1(player1, 6);
        playGameV1(player2, 4);
        playGameV2(player2, 4);

        uint256 player1BalanceBefore = weth.balanceOf(player1);
        uint256 player2BalanceBefore = weth.balanceOf(player2);

        vm.warp(block.timestamp + treasury.TIMELOCK_DURATION());

        manager.collectAndDistribute(POSITION_ID);

        // Verify treasury manager has funds
        uint256 donatedAmount = (wethBalance * treasury.donationShare()) / 100;
        uint256 distributedWeth = wethBalance - donatedAmount;

        assertEq(weth.balanceOf(address(manager)), distributedWeth);

        // First collection should go to V1 players
        uint256 player1Game1Share = manager.withdrawable(
            player1,
            address(weth)
        );
        uint256 player2Game1Share = manager.withdrawable(
            player2,
            address(weth)
        );

        assertEq(player1Game1Share + player2Game1Share, distributedWeth);

        manager.withdraw(player1, address(weth));

        assertEq(
            weth.balanceOf(player1),
            player1BalanceBefore + player1Game1Share
        );

        // 60% of remaining amount after donation
        assertEq(player1Game1Share, (distributedWeth * 60) / 100);
        // 40% of remaining amount after donation
        assertEq(player2Game1Share, (distributedWeth * 40) / 100);

        uint256 newWethAmount = 5 ether;

        // Add more WETH to the position
        positionManager.setPosition(
            POSITION_ID,
            address(infiniteToken),
            address(weth),
            0,
            newWethAmount
        );

        weth.transfer(address(positionManager), newWethAmount);

        // This should all go to the V2 game (only player 2)
        manager.collectAndDistribute(POSITION_ID);

        uint256 newWethDistributedAmount = newWethAmount -
            (newWethAmount * treasury.donationShare()) /
            100;

        // Verify V2 player can now withdraw
        uint256 player2Withdrawable = manager.withdrawable(
            player2,
            address(weth)
        );

        assertEq(
            player2Withdrawable,
            player2Game1Share + newWethDistributedAmount
        );

        manager.withdraw(player2, address(weth));

        assertEq(
            weth.balanceOf(player2),
            player2BalanceBefore + player2Withdrawable
        );
    }

    function test_RevertOnInsufficientBalance() public {
        vm.expectRevert(InfiniteTreasuryManager.InsufficientBalance.selector);
        manager.withdraw(player1, address(weth));
    }

    function test_RevertOnZeroAddress() public {
        vm.expectRevert(InfiniteTreasuryManager.ZeroAddress.selector);
        manager.withdraw(address(0), address(weth));
    }

    function test_WithdrawableCalculation() public {
        // Setup different play counts and fee amounts
        playGameV1(player1, 10); // 10 plays in V1
        playGameV1(player2, 20); // 20 plays in V1

        vm.warp(block.timestamp + treasury.TIMELOCK_DURATION());

        manager.collectAndDistribute(POSITION_ID);

        // Player2 should have ~2x the withdrawable amount of player1
        uint256 player1Share = manager.withdrawable(player1, address(weth));
        uint256 player2Share = manager.withdrawable(player2, address(weth));

        assertApproxEqRel(player2Share, player1Share * 2, 0.01e18); // Within 1% due to rounding
    }

    receive() external payable {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

import "./MockERC20.sol";

contract MockNonfungiblePositionManager {
    mapping(uint256 => address) public ownerOf;
    mapping(uint256 => Position) private _positions;

    struct Position {
        address token0;
        address token1;
        uint256 fees0;
        uint256 fees1;
    }

    function setOwner(uint256 tokenId, address owner) external {
        ownerOf[tokenId] = owner;
    }

    // This matches Uniswap's full interface
    function positions(
        uint256 tokenId
    )
        external
        view
        returns (
            uint96 nonce,
            address operator,
            address token0,
            address token1,
            uint24 fee,
            int24 tickLower,
            int24 tickUpper,
            uint128 liquidity,
            uint256 feeGrowthInside0LastX128,
            uint256 feeGrowthInside1LastX128,
            uint128 tokensOwed0,
            uint128 tokensOwed1
        )
    {
        Position memory pos = _positions[tokenId];
        return (
            0, // nonce
            address(0), // operator
            pos.token0,
            pos.token1,
            3000, // fee
            0, // tickLower
            0, // tickUpper
            0, // liquidity
            0, // feeGrowthInside0LastX128
            0, // feeGrowthInside1LastX128
            uint128(pos.fees0), // tokensOwed0
            uint128(pos.fees1) // tokensOwed1
        );
    }

    function setPosition(
        uint256 tokenId,
        address token0,
        address token1,
        uint256 fees0,
        uint256 fees1
    ) external {
        _positions[tokenId] = Position(token0, token1, fees0, fees1);
    }

    function collect(
        INonfungiblePositionManager.CollectParams calldata params
    ) external returns (uint256 amount0, uint256 amount1) {
        Position storage pos = _positions[params.tokenId];
        require(
            pos.token0 != address(0) && pos.token1 != address(0),
            "Invalid position"
        );

        amount0 = pos.fees0;
        amount1 = pos.fees1;

        if (amount0 > 0) {
            MockERC20(pos.token0).transfer(params.recipient, amount0);
        }
        if (amount1 > 0) {
            MockERC20(pos.token1).transfer(params.recipient, amount1);
        }

        // Reset fees after collection
        pos.fees0 = 0;
        pos.fees1 = 0;
    }
}

Tests: InfiniteGame2 & InfiniteTreasuryManager