// File: contracts/LpLocker.sol



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

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "contracts/IUniswapV2Factory.sol";
import "contracts/INonfungiblePositionManager.sol";

contract LpLocker is ReentrancyGuard {
    address public owner;
    bool public pause;

    constructor(){
        owner = msg.sender;
        pause = false;
        //添加uniV2
        bytes memory _dex = bytes("uniswap");
        bytes memory _ver = bytes("v2");
        LpAddrsSet[_dex][_ver] = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
        emit SetFactoryAddress(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f, "uniswap", "v2");

        // 添加uniV3
        _dex = bytes("uniswap");
        _ver = bytes("v3");
        LpAddrsSet[_dex][_ver] = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
        emit SetFactoryAddress(0x1F98431c8aD98523631AE4a59f267346ea31F984, "uniswap", "v3");
    }

    receive() external payable {}

    uint256 constant public PERCENT_DENOM = 1000;
    string constant public LP_V2 = "v2";
    string constant public LP_V3 = "v3";

    enum LockType {
        FixTokenLock,
        LinearTokenLock,
        LpV2Lock,
        LpV3Lock
    }

    enum LockerStatus {
        Locked,
        PendingRelease,
        Release
    }

    mapping (address => uint256) public IncomeSet;
    mapping (LockType => FeeRates) public FeesSet;
    mapping (address => mapping (bytes32 => LockerUnit)) public LockerSet;
    mapping (bytes => mapping (bytes => address)) public LpAddrsSet;

    struct FeeRates {
        bool created;
        uint256 lockOp;
        uint256 relockOp;
        uint256 transferOp;
        uint256 increaseOp;
        uint256 splitOp;
        uint256 extractOp;
    }

    struct LockerUnit {
        address owner;
        address token;
        string setName;
        uint256 lockAmount;
        uint256 cumulateClaimed;
        uint256 startTime;
        uint256 endTime;
        string dex;
        uint256 nftId;
        uint256 releasePerSecond;
        bool withdrawed;
        LockType lockType;
        LockerStatus status;
    }

    event SetNewOwner(
        address newOwner,
        address oldOwner
    );

    event SetFeeRates(
        LockType lockType,
        uint256 lockOp,
        uint256 relockOp,
        uint256 transferOp,
        uint256 increaseOp,
        uint256 splitOp,
        uint256 extractOp
    );

    event ExtractFees(
        address token,
        string feeType,
        uint256 amount
    );

    event IncreaseIncome(
        address token,
        uint256 amount
    );

    event DecreasetIncome(
        address token,
        uint256 amount
    );

    event CreateTokenLocker(
        bytes32 id,
        address token,
        string setName,
        address forWho,
        uint256 lockAmount,
        uint256 startTime,
        uint256 endTime,
        uint256 releasePerSecond,
        LockType lockType
    );

    event CreateLpLocker(
        bytes32 id,
        address token,
        string setName,
        address forWho,
        uint256 lockAmount,
        uint256 nftId,
        uint256 startTime,
        uint256 endTime,
        string dex,
        LockType lockType
    );

    event SetFactoryAddress(
        address addr,
        string dex,
        string version
    );

    event WithdrawToken(
        bytes32 lockId,
        uint256 amount,
        bool clearAll,
        LockType lockType
    );

    event TransferOwnership(
        address oldOwner,
        address newOwner,
        bytes32 oldLockId,
        bytes32 newLockId,
        string newSetName
    );

    event Relock(
        bytes32 lockId,
        uint256 timeUnix,
        LockType lockType
    );

    event IncreaseLockAmount(
        bytes32 lockId,
        address tokenAddr,
        uint256 lockAmount,
        LockType lockType
    );

    event SplitLocker(
        bytes32 originLockId,
        uint256 originLeftAmount,
        uint256 originRelease,
        bytes32 spliteLockId,
        uint256 splitAmount,
        uint256 splitRelease,
        string splitSetName,
        LockType lockType
    );

    modifier onlyOwner() {
        require(msg.sender == owner, "Invalid owner address");
        _;
    }

    modifier enablePause() {
        require(!pause, "Contract is paused");
        _;
    }

    function setNewOwner(address newOwner) public onlyOwner {
        owner = newOwner;

        emit SetNewOwner(newOwner, msg.sender);
    }

    function setPause(bool newStatus) public onlyOwner {
        pause = newStatus;
    }

    function setFactoryAddress(
        address addr, 
        string memory dex,
        string memory version
    ) public onlyOwner {
        bytes memory _dex = bytes(dex);
        bytes memory _ver = bytes(version);

        LpAddrsSet[_dex][_ver] = addr;

        emit SetFactoryAddress(addr, dex, version);
    }

    function getFactoryAddress(string memory dex, string memory version) public view returns(address) {
        bytes memory _dex = bytes(dex);
        bytes memory _ver = bytes(version);

        return LpAddrsSet[_dex][_ver];
    }

    function setFeeRates(
        LockType _type,
        uint256 lockOp,
        uint256 relockOp,
        uint256 transferOp,
        uint256 increaseOp,
        uint256 splitOp,
        uint256 extractOp
    ) public onlyOwner {
        FeeRates storage feeRates = FeesSet[_type];

        feeRates.created = true;
        feeRates.lockOp = lockOp;
        feeRates.relockOp = relockOp;
        feeRates.transferOp = transferOp;
        feeRates.increaseOp = increaseOp;
        feeRates.splitOp = splitOp;
        feeRates.extractOp = extractOp;

        emit SetFeeRates(_type, lockOp, relockOp, transferOp, increaseOp, splitOp, extractOp);
    }

    function isExistFeeRates(LockType _type) public view returns(bool) {
        return FeesSet[_type].created;
    }

    function getFeeRates(LockType _type) public view returns(FeeRates memory ) {
        return  FeesSet[_type];
    }

    function extractFees(address _token, uint256 amount) public onlyOwner {
        require(amount > 0, "Invalid extract amount");
       
        string memory _type;
        if (_token != address(0)) {
            require(amount <= IERC20(_token).balanceOf(address(this)), "Exceed the ERC20 balance");

            IncomeSet[_token] -= amount;
            _type = "ERC20";
            
            IERC20(_token).transfer(msg.sender, amount);
        } else {
            require(amount <= address(this).balance, "Exceed the ETH balance");
            
            IncomeSet[_token] -= amount;
            _type = "ETH";

            payable(msg.sender).transfer(amount); 
        }

        emit ExtractFees(_token, _type, amount);
    }

    function balanceOfFees(address _token) public view returns(uint256){
        return IncomeSet[_token];
    }

    function generateLockID(
        address tokenAddr, 
        address _owner, 
        string memory setName
    ) public pure returns(bytes32) {
        return keccak256(
            abi.encodePacked(
                tokenAddr, 
                _owner, 
                setName
            )
        );
    }

    function isExistLockID(
        address tokenAddr, 
        address _owner, 
        string memory setName
    ) public view returns(bool) {
        bytes32 id = generateLockID(tokenAddr, _owner, setName);
        LockerUnit storage unit = LockerSet[msg.sender][id];
        if (unit.owner == address(0)) return false;

        return true;
    }

    function withdrawAvaible(bytes32 lockId, address _owner) public view returns(uint256) {
        LockerUnit storage lockUnit = LockerSet[_owner][lockId];
        if(lockUnit.owner != _owner) return 0;

        uint256 startTime = lockUnit.startTime;
        uint256 endTime = lockUnit.endTime;
        uint256 nowUnix = block.timestamp;

        uint256 withdrawAmount;
        if(lockUnit.lockType == LockType.LinearTokenLock) {
            if(nowUnix > endTime) nowUnix = endTime;
            
            withdrawAmount = (nowUnix - startTime) *
                     lockUnit.releasePerSecond -
                      lockUnit.cumulateClaimed;

        } else if(lockUnit.lockType == LockType.LpV3Lock) {
            if(nowUnix <= endTime) return  0;
            withdrawAmount = 1;
        } else {
            if(nowUnix <= endTime) return  0;
            withdrawAmount = lockUnit.lockAmount;
        }

        return withdrawAmount;
    }

    function createTokenLocker(
        address tokenAddr,
        uint256 lockAmount,
        string memory setName,
        address forWho,
        uint256 endTime,
        LockType _type
    ) external nonReentrant enablePause {
        require(isExistFeeRates(_type), "Set up fee rates");
        require(forWho != address(0), "Not support zero address");
        require(bytes(setName).length > 0, "Value of setName must be set");
        require(_type == LockType.FixTokenLock || _type == LockType.LinearTokenLock, "Invalid token locker type");
        require(lockAmount <= IERC20(tokenAddr).balanceOf(msg.sender), "Not enough balance");
        require(!isExistLockID(tokenAddr, forWho, setName), "Locker ID already exist, how about use other name");

        uint256 nowUnix = block.timestamp;
        require(endTime > nowUnix, "Invalid unix end time");

        bytes32 lockId = generateLockID(tokenAddr, forWho, setName);
        
        FeeRates storage feeRates = FeesSet[_type];
        uint256 leftAmount = lockAmount - lockAmount * feeRates.lockOp / PERCENT_DENOM;
        IncomeSet[tokenAddr] += lockAmount * feeRates.lockOp / PERCENT_DENOM;

        uint256 _releaseAmount;
        if(_type == LockType.LinearTokenLock) _releaseAmount = leftAmount / (endTime - nowUnix);

        LockerUnit storage lockUnit = LockerSet[forWho][lockId];
        lockUnit.owner = forWho;
        lockUnit.token = tokenAddr;
        lockUnit.setName = setName;
        lockUnit.lockAmount = leftAmount;
        lockUnit.startTime = nowUnix;
        lockUnit.endTime = endTime;
        lockUnit.releasePerSecond = _releaseAmount;
        lockUnit.lockType = _type;
        lockUnit.withdrawed = false;

        if (_type == LockType.FixTokenLock) {
            lockUnit.status = LockerStatus.Locked;
        } else {
            lockUnit.status = LockerStatus.PendingRelease;
        }

        IERC20(tokenAddr).transferFrom(msg.sender, address(this), lockAmount);

        emit CreateTokenLocker(
            lockId, tokenAddr, setName, 
            forWho, leftAmount, 
            nowUnix, endTime, 
            _releaseAmount, _type
        );
    }

    function createLpLocker(
        address tokenAddr,
        address token0,
        address token1,
        uint256 lockAmount,
        uint256 nftId,
        string memory setName,
        address forWho,
        uint256 endTime,
        string memory dex,
        LockType _type
    ) external payable enablePause nonReentrant {
        require(isExistFeeRates(_type), "Set up fee rates");
        require(forWho != address(0), "Not support zero address");
        require(bytes(setName).length > 0, "Value of setName must be set");
        require(!isExistLockID(tokenAddr, forWho, setName), "Locker ID already exist, how about use other name");

        uint256 nowUnix = block.timestamp;
        require(endTime > nowUnix, "Invalid unix end time");

        FeeRates storage feeRates = FeesSet[_type];
        if (_type == LockType.LpV2Lock) {
            require(lockAmount <= IERC20(tokenAddr).balanceOf(msg.sender), "Not enough balance");

            address factory = getFactoryAddress(dex, LP_V2);
            require(factory != address(0), "Invalid dex value");

            require(tokenAddr == IUniswapV2Factory(factory).getPair(token0, token1), "Illegal LP V2 token address");

            IncomeSet[tokenAddr] += lockAmount * feeRates.lockOp / PERCENT_DENOM;

            IERC20(tokenAddr).transferFrom(msg.sender, address(this), lockAmount);
        } else if (_type == LockType.LpV3Lock) {
            require(msg.value >= feeRates.lockOp,"Not enough fee");
            require(
                INonfungiblePositionManager(tokenAddr).ownerOf(nftId) == msg.sender, 
                "Invalid V3 liquidity nft owner"
            );

            (, ,address _token0, address _token1,,,,,,,, ) = INonfungiblePositionManager(tokenAddr).positions(nftId);
            require(token0 == _token0, "Invalid token0");
            require(token1 == _token1, "Invalid token1");

            IncomeSet[address(0)] +=  msg.value;

            INonfungiblePositionManager(tokenAddr).transferFrom(msg.sender, address(this), nftId);
        } else {
            revert("Invalid locker type");
        }

        bytes32 lockId = generateLockID(tokenAddr, forWho, setName);
        LockerUnit storage lockUnit = LockerSet[forWho][lockId];
        lockUnit.owner = forWho;
        lockUnit.token = tokenAddr;
        lockUnit.setName = setName;
        lockUnit.lockAmount = _type == LockType.LpV3Lock ? 1 : lockAmount - lockAmount * feeRates.lockOp / PERCENT_DENOM;
        lockUnit.cumulateClaimed = 0;
        lockUnit.startTime = nowUnix;
        lockUnit.endTime = endTime;
        lockUnit.dex = dex;
        lockUnit.nftId = nftId;
        lockUnit.lockType = _type;
        lockUnit.status = LockerStatus.Locked;
        lockUnit.withdrawed = false;

        emit CreateLpLocker(
            lockId, tokenAddr, setName, 
            forWho, lockUnit.lockAmount, nftId, nowUnix, 
            endTime, dex, _type
        );
    }

    function withdrawToken(
        bytes32 lockId
    ) external payable nonReentrant {
        LockerUnit storage lockUnit = LockerSet[msg.sender][lockId];
        require(!lockUnit.withdrawed, "Can not withdraw repeatedly");

        require(isExistFeeRates(lockUnit.lockType), "Set up fee rates");
        require(msg.sender == lockUnit.owner, "Invalid locker owner");

        FeeRates storage feeRates = FeesSet[lockUnit.lockType];
        if (lockUnit.lockType == LockType.LpV3Lock) {
            require(msg.value >= feeRates.extractOp, "Not enough fee of extract");
        }

        uint256 withdrawAmount;
        if(lockUnit.lockType == LockType.LpV3Lock) {
            require(withdrawAvaible(lockId, msg.sender) > 0, "Can not withdraw before expiration");

            lockUnit.withdrawed = true;
            withdrawAmount = 1;

            IncomeSet[address(0)] += msg.value;

            IERC721(lockUnit.token).transferFrom(address(this), lockUnit.owner, lockUnit.nftId);
        } else {
            uint256 amount = withdrawAvaible(lockId, msg.sender);
            require(amount > 0, "Can not withdraw before expiration");

            lockUnit.cumulateClaimed += amount;

            if(lockUnit.lockType == LockType.LinearTokenLock) {
                if(withdrawAvaible(lockId, msg.sender) == 0) lockUnit.withdrawed = true;
            } else {
                lockUnit.withdrawed = true;
            }

            uint256 fee = amount * feeRates.extractOp / PERCENT_DENOM;
            uint256 leftAmount = amount-fee;

            IERC20(lockUnit.token).transfer(msg.sender, leftAmount);
            
            IncomeSet[lockUnit.token] += fee;

            withdrawAmount = leftAmount;
        }
        
        emit WithdrawToken(lockId, withdrawAmount, lockUnit.withdrawed ,lockUnit.lockType);
    }

    function transferOwnership(
        bytes32 lockId,
        address newOwner,
        string memory newSetName
    ) external payable nonReentrant {
        LockerUnit storage lockUnit = LockerSet[msg.sender][lockId];

        require(!lockUnit.withdrawed, "Lock already unlocked");
        
        require(msg.sender == lockUnit.owner, "Invalid locker owner");
        require(isExistFeeRates(lockUnit.lockType), "Set up fee rates");

        require(!isExistLockID(lockUnit.token, newOwner, newSetName), "Locker ID already exist, how about use other name");

        FeeRates storage feeRates = FeesSet[lockUnit.lockType];
        require(msg.value >= feeRates.transferOp, "Not enough fee for transfer");

        bytes32 newID = generateLockID(lockUnit.token, newOwner, newSetName);
        lockUnit.owner = newOwner;
        LockerSet[newOwner][newID] = lockUnit;

        delete LockerSet[msg.sender][lockId];

        IncomeSet[address(0)] += msg.value;

        emit TransferOwnership(msg.sender, newOwner, lockId, newID, newSetName);
    }

    function relock(
        bytes32 lockId,
        uint256 timeUnix
    ) external payable enablePause nonReentrant {
        LockerUnit storage lockUnit = LockerSet[msg.sender][lockId];

        require(!lockUnit.withdrawed, "Lock already unlocked");

        require(msg.sender == lockUnit.owner, "Invalid locker owner");
        require(isExistFeeRates(lockUnit.lockType), "Set up fee rates");

        FeeRates storage feeRates = FeesSet[lockUnit.lockType];

        require(msg.value >= feeRates.relockOp, "Not enough fee of relock");
        require(timeUnix > lockUnit.endTime, "New timestamp must bigger than old");

        if (lockUnit.lockType == LockType.LinearTokenLock) {
            lockUnit.releasePerSecond = (lockUnit.lockAmount-lockUnit.cumulateClaimed) / (timeUnix - lockUnit.startTime);
        }

        lockUnit.endTime = timeUnix;

        IncomeSet[address(0)] += msg.value;

        emit Relock(lockId, timeUnix, lockUnit.lockType);
    }

    function increaseLockAmount(
        bytes32 lockId,
        address tokenAddr,
        uint256 increaseAmount
    ) external enablePause nonReentrant {
        LockerUnit storage lockUnit = LockerSet[msg.sender][lockId];

        require(!lockUnit.withdrawed, "Lock already unlocked");

        require(msg.sender == lockUnit.owner, "Invalid locker owner");
        require(isExistFeeRates(lockUnit.lockType), "Set up fee rates");
        require(lockUnit.lockType != LockType.LpV3Lock, "Not support LP V3 token");
        require(increaseAmount > 0, "Must be large than 0");

        FeeRates storage feeRates = FeesSet[lockUnit.lockType];
        uint256 fee = increaseAmount * feeRates.increaseOp / PERCENT_DENOM;
        IncomeSet[tokenAddr] += fee;

        uint256 leftAmount = increaseAmount - fee;
        lockUnit.lockAmount += leftAmount;

        if (lockUnit.lockType == LockType.LinearTokenLock) {
            lockUnit.releasePerSecond = (lockUnit.lockAmount-lockUnit.cumulateClaimed) / (lockUnit.endTime - lockUnit.startTime);
        }

        IERC20(tokenAddr).transferFrom(msg.sender, address(this), increaseAmount);

        emit IncreaseLockAmount(lockId, tokenAddr, leftAmount, lockUnit.lockType);
    }

    function splitLocker(
        bytes32 lockId,
        address tokenAddr,
        uint256 splitAmount,
        string memory splitSetName
    ) external payable enablePause nonReentrant {
        LockerUnit storage lockUnit = LockerSet[msg.sender][lockId];

        require(!lockUnit.withdrawed, "Lock already unlocked");

        require(msg.sender == lockUnit.owner, "Invalid locker owner");
        require(isExistFeeRates(lockUnit.lockType), "Set up fee rates");
        require(lockUnit.lockType != LockType.LpV3Lock, "Not support LP V3 token");
        require(tokenAddr == lockUnit.token, "Invalid token address");
        require(!isExistLockID(lockUnit.token, lockUnit.owner, splitSetName), "Locker ID already exist, how about use other name");

        FeeRates storage feeRates = FeesSet[lockUnit.lockType];

        require(msg.value >= feeRates.splitOp, "Not enough splitOp fee");
        require(splitAmount < lockUnit.lockAmount, "Split amount is larger than lock amount");

        bytes32 splitLockId = generateLockID(tokenAddr, msg.sender, splitSetName);
        LockerUnit storage lockUnit2 = LockerSet[msg.sender][splitLockId];
        lockUnit2.owner = lockUnit.owner;
        lockUnit2.token = lockUnit.token;
        lockUnit2.setName = lockUnit.setName;
        lockUnit2.startTime = lockUnit.startTime;
        lockUnit2.endTime = lockUnit.endTime;
        lockUnit2.dex = lockUnit.dex;
        lockUnit2.releasePerSecond = lockUnit.releasePerSecond * splitAmount / lockUnit.lockAmount;
        lockUnit2.lockType = lockUnit.lockType;
        lockUnit2.lockAmount = splitAmount;
        lockUnit2.withdrawed = false;

        if (lockUnit.lockType == LockType.LinearTokenLock) {
            lockUnit2.cumulateClaimed = lockUnit.cumulateClaimed * splitAmount / lockUnit.lockAmount;
        } else {
            lockUnit2.cumulateClaimed = lockUnit.cumulateClaimed;
        }

        lockUnit.lockAmount -= splitAmount;
        lockUnit.cumulateClaimed -= lockUnit2.cumulateClaimed;
        lockUnit.releasePerSecond -= lockUnit2.releasePerSecond;

        IncomeSet[address(0)] += msg.value;

        emit SplitLocker(lockId, lockUnit.lockAmount, lockUnit.releasePerSecond, splitLockId, splitAmount, lockUnit2.releasePerSecond, splitSetName, lockUnit.lockType);
    }
}



// File: contracts/INonfungiblePositionManager.sol



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

import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol";

interface IPoolInitializer {
    function createAndInitializePoolIfNecessary(
        address token0,
        address token1,
        uint24 fee,
        uint160 sqrtPriceX96
    ) external payable returns (address pool);
}

interface IERC721Permit is IERC721 {
    function PERMIT_TYPEHASH() external pure returns (bytes32);

    function DOMAIN_SEPARATOR() external view returns (bytes32);

    function permit(
        address spender,
        uint256 tokenId,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external payable;
}

interface IPeripheryPayments {
    function unwrapWETH9(uint256 amountMinimum, address recipient) external payable;

    function refundETH() external payable;

    function sweepToken(
        address token,
        uint256 amountMinimum,
        address recipient
    ) external payable;
}

interface IPeripheryImmutableState {
    function factory() external view returns (address);

    function WETH9() external view returns (address);
}

// library PoolAddress {
//     bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

//     // /// @notice The identifying key of the pool
//     // struct PoolKey {
//     //     address token0;
//     //     address token1;
//     //     uint24 fee;
//     // }

//     /// @notice Returns PoolKey: the ordered tokens with the matched fee levels
//     /// @param tokenA The first token of a pool, unsorted
//     /// @param tokenB The second token of a pool, unsorted
//     /// @param fee The fee level of the pool
//     /// @return Poolkey The pool details with ordered token0 and token1 assignments
//     // function getPoolKey(
//     //     address tokenA,
//     //     address tokenB,
//     //     uint24 fee
//     // ) internal pure returns (PoolKey memory) {
//     //     if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA);
//     //     return PoolKey({token0: tokenA, token1: tokenB, fee: fee});
//     // }

//     // /// @notice Deterministically computes the pool address given the factory and PoolKey
//     // /// @param factory The Uniswap V3 factory contract address
//     // /// @param key The PoolKey
//     // /// @return pool The contract address of the V3 pool
//     // function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
//     //     require(key.token0 < key.token1);
//     //     pool = address(
//     //         uint256(
//     //             keccak256(
//     //                 abi.encodePacked(
//     //                     hex'ff',
//     //                     factory,
//     //                     keccak256(abi.encode(key.token0, key.token1, key.fee)),
//     //                     POOL_INIT_CODE_HASH
//     //                 )
//     //             )
//     //         )
//     //     );
//     // }
// }


interface INonfungiblePositionManager is
    IPoolInitializer,
    IPeripheryPayments,
    IPeripheryImmutableState,
    IERC721Metadata,
    IERC721Enumerable,
    IERC721Permit
{
    event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
    event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
    event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1);

    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
        );

    struct MintParams {
        address token0;
        address token1;
        uint24 fee;
        int24 tickLower;
        int24 tickUpper;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        address recipient;
        uint256 deadline;
    }

    function mint(MintParams calldata params)
        external
        payable
        returns (
            uint256 tokenId,
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        );

    struct IncreaseLiquidityParams {
        uint256 tokenId;
        uint256 amount0Desired;
        uint256 amount1Desired;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }
    
    function increaseLiquidity(IncreaseLiquidityParams calldata params)
        external
        payable
        returns (
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        );

    struct DecreaseLiquidityParams {
        uint256 tokenId;
        uint128 liquidity;
        uint256 amount0Min;
        uint256 amount1Min;
        uint256 deadline;
    }

    function decreaseLiquidity(DecreaseLiquidityParams calldata params)
        external
        payable
        returns (uint256 amount0, uint256 amount1);

    struct CollectParams {
        uint256 tokenId;
        address recipient;
        uint128 amount0Max;
        uint128 amount1Max;
    }

    function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);

    function burn(uint256 tokenId) external payable;
}

// File: contracts/IUniswapV2Factory.sol



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

interface IUniswapV2Factory {
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);

    function feeTo() external view returns (address);
    function feeToSetter() external view returns (address);

    function getPair(address tokenA, address tokenB) external view returns (address pair);
    function allPairs(uint) external view returns (address pair);
    function allPairsLength() external view returns (uint);

    function createPair(address tokenA, address tokenB) external returns (address pair);

    function setFeeTo(address) external;
    function setFeeToSetter(address) external;
}
// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.20;

import {IERC721} from "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

// File: @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/IERC721Metadata.sol)

pragma solidity ^0.8.20;

import {IERC721} from "../IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Metadata is IERC721 {
    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.20;

import {IERC165} from "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
     *   a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or
     *   {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
     *   a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the address zero.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}