// File: src/AlignmentVault.sol



// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.20;

import "solady/src/auth/Ownable.sol";
import "openzeppelin/interfaces/IERC20.sol";
import "openzeppelin/interfaces/IERC721.sol";
import "openzeppelin/proxy/utils/Initializable.sol";
import "liquidity-helper/UniswapV2LiquidityHelper.sol";

interface INFTXFactory {
    function vaultsForAsset(address asset) external view returns (address[] memory);
}

interface INFTXVault {
    function vaultId() external view returns (uint256);
}

interface INFTXLPStaking {
    function deposit(uint256 vaultId, uint256 amount) external;
    function claimRewards(uint256 vaultId) external;
}

interface INFTXStakingZap {
    function addLiquidity721(uint256 vaultId, uint256[] calldata ids, uint256 minWethIn, uint256 wethIn)
        external
        returns (uint256);
}

/**
 * @title AlignmentVault
 * @notice This allows anything to send ETH to a vault for the purpose of permanently deepening the floor liquidity of a target NFT collection.
 * While the liquidity is locked forever, the yield can be claimed indefinitely.
 * @dev You must initialize this contract once deployed! There is a factory for this, use it!
 * @author Zodomo.eth (X: @0xZodomo, Telegram: @zodomo, GitHub: Zodomo, Email: zodomo@proton.me)
 */
contract AlignmentVault is Ownable, Initializable {
    error InsufficientFunds();
    error InvalidVaultId();
    error AlignedAsset();
    error NoNFTXVault();
    error UnwantedNFT();

    IWETH internal constant _WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    address internal constant _SUSHI_V2_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac;
    IUniswapV2Router02 internal constant _SUSHI_V2_ROUTER =
        IUniswapV2Router02(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);

    INFTXFactory internal constant _NFTX_VAULT_FACTORY = INFTXFactory(0xBE86f647b167567525cCAAfcd6f881F1Ee558216);
    INFTXLPStaking internal constant _NFTX_LIQUIDITY_STAKING =
        INFTXLPStaking(0x688c3E4658B5367da06fd629E41879beaB538E37);
    INFTXStakingZap internal constant _NFTX_STAKING_ZAP = INFTXStakingZap(0xdC774D5260ec66e5DD4627E1DD800Eff3911345C);

    UniswapV2LiquidityHelper internal _liqHelper; // Liquidity helper used to deepen NFTX SLP with any amount of tokens
    IERC721 public erc721; // ERC721 token
    IERC20 public nftxInventory; // NFTX NFT token
    IERC20 public nftxLiquidity; // NFTX NFTWETH token
    uint256 public vaultId; // NFTX vault Id
    uint256[] public nftsHeld; // Inventory of aligned erc721 NFTs stored in contract

    constructor() payable {}

    /**
    * @notice Initializes all contract variables and NFTX integration
    * @param _erc721 Address of the target ERC721 contract
    * @param _owner Address of the owner to be set for this contract
    * @param _vaultId Identifier for the NFTX vault. If set to 0, the default (initial) vault will be used.
    */
    function initialize(address _erc721, address _owner, uint256 _vaultId) external payable virtual initializer {
        // Initialize contract ownership
        _initializeOwner(_owner);
        // Set target NFT collection for alignment
        erc721 = IERC721(_erc721);
        // Approve sending any NFT tokenId to NFTX Staking Zap contract
        erc721.setApprovalForAll(address(_NFTX_STAKING_ZAP), true);
        // Max approve WETH to NFTX LP Staking contract
        IERC20(address(_WETH)).approve(address(_NFTX_STAKING_ZAP), type(uint256).max);
        // Derive vaultId if necessary
        // Loop index is set to max value in order to determine if a match was found
        uint256 index = type(uint256).max;
        // If no vaultId is specified, use default (initial) vault
        if (_vaultId == 0) {
            index = 0;
        } else {
            // Retrieve all vaults
            address[] memory vaults = _NFTX_VAULT_FACTORY.vaultsForAsset(_erc721);
            // Revert if no vaults are returned
            if (vaults.length == 0) revert NoNFTXVault();
            // Search for vaultId
            for (uint256 i; i < vaults.length;) {
                if (INFTXVault(vaults[i]).vaultId() == _vaultId) {
                    index = i;
                    vaultId = _vaultId;
                    break;
                }
                unchecked {
                    ++i;
                }
            }
            // If vaultId wasn't found, revert
            if (index == type(uint256).max) revert InvalidVaultId();
        }
        // Derive nftxInventory token contract and vaultId if necessary
        address _nftxInventory = _NFTX_VAULT_FACTORY.vaultsForAsset(_erc721)[index];
        if (_vaultId == 0) vaultId = uint64(INFTXVault(_nftxInventory).vaultId());
        nftxInventory = IERC20(_nftxInventory);
        // Derive nftxLiquidity LP contract
        nftxLiquidity = IERC20(UniswapV2Library.pairFor(_SUSHI_V2_FACTORY, address(_WETH), _nftxInventory));
        // Approve sending nftxLiquidity to NFTX LP Staking contract
        nftxLiquidity.approve(address(_NFTX_LIQUIDITY_STAKING), type(uint256).max);
        // Setup liquidity helper
        _liqHelper = new UniswapV2LiquidityHelper(_SUSHI_V2_FACTORY, address(_SUSHI_V2_ROUTER), address(_WETH));
        // Approve tokens to liquidity helper
        IERC20(address(_WETH)).approve(address(_liqHelper), type(uint256).max);
        nftxInventory.approve(address(_liqHelper), type(uint256).max);
    }

    /**
    * @notice Disables the ability to call initialization functions again, recommended post-initialization
    */
    function disableInitializers() external payable virtual {
        _disableInitializers();
    }

    /**
    * @notice The ability to renounce is overridden as it would break the vault. A privileged caller is required.
    */
    function renounceOwnership() public payable virtual override {}

    /**
    * @notice Estimate the floor price of the NFT in terms of WETH based on NFTX SLP reserves
    * @return spotPrice The estimated price of the NFT token in WETH
    */
    function _estimateFloor() internal view virtual returns (uint256 spotPrice) {
        // Retrieve SLP reserves to calculate price of NFT token in WETH
        (uint112 reserve0, uint112 reserve1,) = IUniswapV2Pair(address(nftxLiquidity)).getReserves();
        // Calculate value of NFT spot in WETH using SLP reserves values
        // Reverse reserve values if token1 isn't WETH
        if (IUniswapV2Pair(address(nftxLiquidity)).token1() != address(_WETH)) {
            spotPrice = ((10 ** 18 * uint256(reserve0)) / uint256(reserve1));
        } else {
            spotPrice = ((10 ** 18 * uint256(reserve1)) / uint256(reserve0));
        }
        return (spotPrice);
    }

    /**
    * @notice Wrap all ETH, if any, before function execution
    */
    function _wrapEth() internal virtual {
        // Wrap all ETH, if any
        uint256 balance = address(this).balance;
        if (balance > 0) _WETH.deposit{value: balance}();
    }

    /**
    * @notice Stake all LP tokens, if any
    */
    function _stakeLiquidity() internal virtual {
        uint256 liquidity = nftxLiquidity.balanceOf(address(this));
        if (liquidity > 0) _NFTX_LIQUIDITY_STAKING.deposit(vaultId, liquidity);
    }

    /**
    * @notice Add aligned NFTs to NFTX vault by pairing them with their floor price in ETH
    * @dev This will revert if the contract doesn't hold the NFT. This doesn't require checkInventory().
    * @param _tokenIds Array of specific NFTs to try and add to the vault
    */
    function alignNfts(uint256[] memory _tokenIds) external payable virtual onlyOwner {
        // Revert if empty _tokenIds array is passed
        if (_tokenIds.length == 0) revert();
        // Wrap all ETH, if any
        _wrapEth();
        // Retrieve total WETH balance
        uint256 balance = IERC20(address(_WETH)).balanceOf(address(this));
        // Retrieve NFTX LP price for one NFT
        uint256 floorPrice = _estimateFloor();
        // Add 1 to floorPrice in order to resolve liquidity rounding issue
        uint256 afford = balance / (floorPrice + 1);
        // Revert if we cannot afford this amount of NFTs
        if (afford < _tokenIds.length) revert InsufficientFunds();
        // Calculate exact ETH to add to LP with NFTs
        uint256 requiredEth = _tokenIds.length * (floorPrice + 1);
        // Stake NFTs and ETH, approvals were given in initializeVault()
        _NFTX_STAKING_ZAP.addLiquidity721(vaultId, _tokenIds, 1, requiredEth);
        // Stake any held liquidity tokens
        _stakeLiquidity();
        // Purge tokenIds if they exist in nftsHeld inventory
        for (uint256 i; i < _tokenIds.length;) {
            // Cache nftsHeld for each aligned tokenId as its length will change upon each pop
            uint256[] memory inventory = nftsHeld;
            for (uint256 j; j < inventory.length;) {
                if (inventory[j] == _tokenIds[i]) {
                    nftsHeld[j] = nftsHeld[inventory.length - 1];
                    nftsHeld.pop();
                }
                unchecked { ++j; }
            }
            unchecked { ++i; }
        }
    }

    /**
    * @notice Add specific amount of ETH/WETH and all fractionalized NFT tokens to NFTX vault
    * @dev Any ETH (msg.value or address(this).balance) will be wrapped to WETH before processing
    * @param _amount is the total amount of WETH to add
    */
    function alignTokens(uint256 _amount) external payable virtual onlyOwner {
        // Wrap all ETH, if any
        _wrapEth();
        // Retrieve total WETH balance
        uint256 balance = IERC20(address(_WETH)).balanceOf(address(this));
        // Revert if _amount is over balance
        if (_amount > balance) revert InsufficientFunds();
        // Cache nftxInventory to prevent a double SLOAD
        uint256 nftxInvBal = nftxInventory.balanceOf(address(this));
        // Process rebalancing remaining ETH and inventory tokens (if any) to add to LP
        if (_amount > 0 || nftxInvBal > 0) {
            _liqHelper.swapAndAddLiquidityTokenAndToken(
                address(_WETH), address(nftxInventory), uint112(_amount), uint112(nftxInvBal), 1, address(this)
            );
        }
        // Stake any held liquidity tokens
        _stakeLiquidity();
    }

    /**
    * @notice Aligns max liquidity by depositing NFTs and ETH into the NFTX vault and staking them
    * This will add as many NFTs as it can afford, before staking the ETH remainder
    * Confirm vault has enough ETH for NFTs held before aligning max liquidity
    */
    function alignMaxLiquidity() external payable virtual onlyOwner {
        // Cache vaultId to save gas
        uint256 _vaultId = vaultId;
        // Wrap all ETH, if any
        _wrapEth();
        // Retrieve total WETH balance
        uint256 balance = IERC20(address(_WETH)).balanceOf(address(this));

        // Retrieve NFTs held
        uint256[] memory inventory = nftsHeld;
        uint256 length = inventory.length;
        // Process adding liquidity using as many NFTs as the ETH balance allows
        if (length > 0) {
            // Retrieve NFTX LP price for 1 full inventory token
            uint256 floorPrice = _estimateFloor();
            // Determine how many NFTs we can afford to add to LP
            // Add 1 to floorPrice in order to resolve liquidity rounding issue
            uint256 afford = balance / (floorPrice + 1);
            uint256 addQty;
            // If we can afford to add more than we have, add what we have, otherwise add what we can afford
            (afford >= length) ? addQty = length : addQty = afford;
            // Add NFTs to LP if we can afford to
            if (addQty > 0) {
                // Calculate exact ETH to add to LP with NFTs
                uint256 requiredEth = addQty * (floorPrice + 1);
                // Iterate through inventory for as many NFTs as we can afford to add
                uint256[] memory tokenIds = new uint256[](addQty);
                for (uint256 i; i < addQty;) {
                    tokenIds[i] = inventory[length - addQty + i];
                    nftsHeld.pop();
                    unchecked {
                        ++i;
                    }
                }
                // Stake NFTs and ETH, approvals were given in initializeVault()
                _NFTX_STAKING_ZAP.addLiquidity721(_vaultId, tokenIds, 1, requiredEth);
                // Update cached balance after adding NFTs to vault
                balance = IERC20(address(_WETH)).balanceOf(address(this));
            }
        }

        // Cache nftxInventory to prevent a double SLOAD
        uint256 nftxInvBal = nftxInventory.balanceOf(address(this));
        // Process rebalancing remaining ETH and inventory tokens (if any) to add to LP
        if (balance > 0 || nftxInvBal > 0) {
            _liqHelper.swapAndAddLiquidityTokenAndToken(
                address(_WETH), address(nftxInventory), uint112(balance), uint112(nftxInvBal), 1, address(this)
            );
        }

        // Stake any held liquidity tokens
        _stakeLiquidity();
    }

    /**
    * @notice Claims yield generated by the staked NFTWETH SLP. Yield can be compounded or split with a recipient.
    * @param _recipient Address to receive 50% of the yield. If address(0), the yield will be compounded.
    */
    function claimYield(address _recipient) external payable virtual onlyOwner {
        // Cache vaultId to save gas
        uint256 _vaultId = vaultId;
        // Claim SLP rewards
        _NFTX_LIQUIDITY_STAKING.claimRewards(_vaultId);
        // Determine yield amount
        uint256 yield = nftxInventory.balanceOf(address(this));
        // If no yield, end execution to save gas
        if (yield == 0) return;
        // If recipient is provided, send them 50%
        if (_recipient != address(0)) {
            uint256 amount;
            unchecked {
                amount = yield / 2;
                yield -= amount;
            }
            nftxInventory.transfer(_recipient, amount);
        }
        // Send all remaining yield to LP
        _liqHelper.swapAndAddLiquidityTokenAndToken(
            address(_WETH), address(nftxInventory), 0, uint112(yield), 1, address(this)
        );
        // Stake all liquidity tokens
        _stakeLiquidity();
    }

    /**
    * @notice Checks the contract's inventory to recognize any new NFTs that were transferred unsafely
    * @param _tokenIds Array of tokenIds to check against the contract's inventory
    */
    function checkInventory(uint256[] memory _tokenIds) external payable virtual {
        // Cache nftsHeld to reduce SLOADs
        uint256[] memory inventory = nftsHeld;
        // Iterate through passed array
        for (uint256 i; i < _tokenIds.length;) {
            // Try check for ownership used in case token has been burned
            try erc721.ownerOf(_tokenIds[i]) {
                // If this address is the owner, see if it is in nftsHeld cached array
                if (erc721.ownerOf(_tokenIds[i]) == address(this)) {
                    bool noticed;
                    for (uint256 j; j < inventory.length;) {
                        // If NFT is found, end loop and iterate to next tokenId
                        if (inventory[j] == _tokenIds[i]) {
                            noticed = true;
                            break;
                        }
                        unchecked {
                            ++j;
                        }
                    }
                    // If tokenId wasn't in stored array, add it
                    if (!noticed) nftsHeld.push(_tokenIds[i]);
                }
            } catch {}
            unchecked {
                ++i;
            }
        }
    }

    /**
    * @notice Retrieve known NFT inventory to check if contract is aware of holdings
    */
    function getInventory() external view virtual returns (uint256[] memory) {
        return nftsHeld;
    }

    /**
    * @notice Allows the owner to rescue ERC20 tokens or ETH from vault and/or liquidity helper.
    * For aligned assets (like ETH, WETH, nftxInventory, nftxLiquidity), the function will rescue 
    * the assets to the vault itself and return 0. For any other tokens, it will rescue from both 
    * the liquidity helper and the vault, and then send the total balance to a specified address.
    * 
    * @param _token The address of the ERC20 token to rescue. Use address(0) for ETH.
    * @param _to The recipient address to send the rescued tokens to.
    * @return amount Returns the amount of tokens sent to the recipient. Returns 0 for aligned assets.
    */
    function rescueERC20(address _token, address _to) external payable virtual onlyOwner returns (uint256 amount) {
        // If address(0), rescue ETH from liq helper to vault
        if (_token == address(0)) {
            _liqHelper.emergencyWithdrawEther();
            uint256 balance = address(this).balance;
            if (balance > 0) _WETH.deposit{value: balance}();
            return (0);
        }
        // If WETH, nftxInventory, or nftxLiquidity, rescue from liq helper to vault
        else if (_token == address(_WETH) || _token == address(nftxInventory) || _token == address(nftxLiquidity)) {
            _liqHelper.emergencyWithdrawErc20(_token);
            return (0);
        }
        // If any other token, rescue from liq helper and/or vault and send to recipient
        else {
            // Retrieve tokens from liq helper, if any
            if (IERC20(_token).balanceOf(address(_liqHelper)) > 0) {
                _liqHelper.emergencyWithdrawErc20(_token);
            }
            // Check updated balance
            uint256 balance = IERC20(_token).balanceOf(address(this));
            // Send entire balance to recipient
            IERC20(_token).transfer(_to, balance);
            return (balance);
        }
    }
    
    /**
    * @notice Allows the owner to rescue non-aligned ERC721 tokens. If the ERC721 token is from the 
    * aligned collection, the transaction is reverted.
    *
    * @param _token The address of the ERC721 token contract.
    * @param _to The recipient address to send the rescued NFT to.
    * @param _tokenId The ID of the NFT to be rescued.
    */
    function rescueERC721(address _token, address _to, uint256 _tokenId) external payable virtual onlyOwner {
        // If _address is for the aligned collection, revert
        if (address(erc721) == _token) revert AlignedAsset();
        // Otherwise, attempt to send to recipient
        else IERC721(_token).transferFrom(address(this), _to, _tokenId);
    }

    /**
    * @notice Fallback function that converts any received ETH to WETH.
    */
    receive() external payable virtual {
        _WETH.deposit{value: msg.value}();
    }
    
    /**
    * @notice Handles the logic when an ERC721 NFT is sent to this contract. Logs only aligned NFTs, 
    * and reverts if any other NFTs are sent.
    *
    * @param _tokenId The ID of the received NFT.
    * @return magicBytes Returns a bytes4 magic value if the NFT transfer is accepted.
    */
    function onERC721Received(address, address, uint256 _tokenId, bytes calldata) external virtual returns (bytes4 magicBytes) {
        if (msg.sender == address(erc721)) nftsHeld.push(_tokenId);
        else revert UnwantedNFT();
        return AlignmentVault.onERC721Received.selector;
    }
}

// File: lib/solady/src/auth/Ownable.sol



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

/// @notice Simple single owner authorization mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)
///
/// @dev Note:
/// This implementation does NOT auto-initialize the owner to `msg.sender`.
/// You MUST call the `_initializeOwner` in the constructor / initializer.
///
/// While the ownable portion follows
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
abstract contract Ownable {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The caller is not authorized to call the function.
    error Unauthorized();

    /// @dev The `newOwner` cannot be the zero address.
    error NewOwnerIsZeroAddress();

    /// @dev The `pendingOwner` does not have a valid handover request.
    error NoHandoverRequest();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                           EVENTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The ownership is transferred from `oldOwner` to `newOwner`.
    /// This event is intentionally kept the same as OpenZeppelin's Ownable to be
    /// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),
    /// despite it not being as lightweight as a single argument event.
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    /// @dev An ownership handover to `pendingOwner` has been requested.
    event OwnershipHandoverRequested(address indexed pendingOwner);

    /// @dev The ownership handover to `pendingOwner` has been canceled.
    event OwnershipHandoverCanceled(address indexed pendingOwner);

    /// @dev `keccak256(bytes("OwnershipTransferred(address,address)"))`.
    uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =
        0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;

    /// @dev `keccak256(bytes("OwnershipHandoverRequested(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =
        0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;

    /// @dev `keccak256(bytes("OwnershipHandoverCanceled(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =
        0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STORAGE                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The owner slot is given by: `not(_OWNER_SLOT_NOT)`.
    /// It is intentionally chosen to be a high value
    /// to avoid collision with lower slots.
    /// The choice of manual storage layout is to enable compatibility
    /// with both regular and upgradeable contracts.
    uint256 private constant _OWNER_SLOT_NOT = 0x8b78c6d8;

    /// The ownership handover slot of `newOwner` is given by:
    /// ```
    ///     mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))
    ///     let handoverSlot := keccak256(0x00, 0x20)
    /// ```
    /// It stores the expiry timestamp of the two-step ownership handover.
    uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                     INTERNAL FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Initializes the owner directly without authorization guard.
    /// This function must be called upon initialization,
    /// regardless of whether the contract is upgradeable or not.
    /// This is to enable generalization to both regular and upgradeable contracts,
    /// and to save gas in case the initial owner is not the caller.
    /// For performance reasons, this function will not check if there
    /// is an existing owner.
    function _initializeOwner(address newOwner) internal virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Clean the upper 96 bits.
            newOwner := shr(96, shl(96, newOwner))
            // Store the new value.
            sstore(not(_OWNER_SLOT_NOT), newOwner)
            // Emit the {OwnershipTransferred} event.
            log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
        }
    }

    /// @dev Sets the owner directly without authorization guard.
    function _setOwner(address newOwner) internal virtual {
        /// @solidity memory-safe-assembly
        assembly {
            let ownerSlot := not(_OWNER_SLOT_NOT)
            // Clean the upper 96 bits.
            newOwner := shr(96, shl(96, newOwner))
            // Emit the {OwnershipTransferred} event.
            log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
            // Store the new value.
            sstore(ownerSlot, newOwner)
        }
    }

    /// @dev Throws if the sender is not the owner.
    function _checkOwner() internal view virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // If the caller is not the stored owner, revert.
            if iszero(eq(caller(), sload(not(_OWNER_SLOT_NOT)))) {
                mstore(0x00, 0x82b42900) // `Unauthorized()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Returns how long a two-step ownership handover is valid for in seconds.
    /// Override to return a different value if needed.
    /// Made internal to conserve bytecode. Wrap it in a public function if needed.
    function _ownershipHandoverValidFor() internal view virtual returns (uint64) {
        return 48 * 3600;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  PUBLIC UPDATE FUNCTIONS                   */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Allows the owner to transfer the ownership to `newOwner`.
    function transferOwnership(address newOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(shl(96, newOwner)) {
                mstore(0x00, 0x7448fbae) // `NewOwnerIsZeroAddress()`.
                revert(0x1c, 0x04)
            }
        }
        _setOwner(newOwner);
    }

    /// @dev Allows the owner to renounce their ownership.
    function renounceOwnership() public payable virtual onlyOwner {
        _setOwner(address(0));
    }

    /// @dev Request a two-step ownership handover to the caller.
    /// The request will automatically expire in 48 hours (172800 seconds) by default.
    function requestOwnershipHandover() public payable virtual {
        unchecked {
            uint256 expires = block.timestamp + _ownershipHandoverValidFor();
            /// @solidity memory-safe-assembly
            assembly {
                // Compute and set the handover slot to `expires`.
                mstore(0x0c, _HANDOVER_SLOT_SEED)
                mstore(0x00, caller())
                sstore(keccak256(0x0c, 0x20), expires)
                // Emit the {OwnershipHandoverRequested} event.
                log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())
            }
        }
    }

    /// @dev Cancels the two-step ownership handover to the caller, if any.
    function cancelOwnershipHandover() public payable virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, caller())
            sstore(keccak256(0x0c, 0x20), 0)
            // Emit the {OwnershipHandoverCanceled} event.
            log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())
        }
    }

    /// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.
    /// Reverts if there is no existing ownership handover requested by `pendingOwner`.
    function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            let handoverSlot := keccak256(0x0c, 0x20)
            // If the handover does not exist, or has expired.
            if gt(timestamp(), sload(handoverSlot)) {
                mstore(0x00, 0x6f5e8818) // `NoHandoverRequest()`.
                revert(0x1c, 0x04)
            }
            // Set the handover slot to 0.
            sstore(handoverSlot, 0)
        }
        _setOwner(pendingOwner);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   PUBLIC READ FUNCTIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Returns the owner of the contract.
    function owner() public view virtual returns (address result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := sload(not(_OWNER_SLOT_NOT))
        }
    }

    /// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.
    function ownershipHandoverExpiresAt(address pendingOwner)
        public
        view
        virtual
        returns (uint256 result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute the handover slot.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            // Load the handover slot.
            result := sload(keccak256(0x0c, 0x20))
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         MODIFIERS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Marks a function as only callable by the owner.
    modifier onlyOwner() virtual {
        _checkOwner();
        _;
    }
}

// File: lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol



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

pragma solidity ^0.8.20;

import {IERC20} from "../token/ERC20/IERC20.sol";

// File: lib/openzeppelin-contracts/contracts/interfaces/IERC721.sol



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

pragma solidity ^0.8.20;

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

// File: lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol



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

pragma solidity ^0.8.20;

/**
 * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
 * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
 * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
 * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
 *
 * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
 * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
 * case an upgrade adds a module that needs to be initialized.
 *
 * For example:
 *
 * [.hljs-theme-light.nopadding]
 * ```solidity
 * contract MyToken is ERC20Upgradeable {
 *     function initialize() initializer public {
 *         __ERC20_init("MyToken", "MTK");
 *     }
 * }
 *
 * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
 *     function initializeV2() reinitializer(2) public {
 *         __ERC20Permit_init("MyToken");
 *     }
 * }
 * ```
 *
 * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
 * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
 *
 * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
 * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
 *
 * [CAUTION]
 * ====
 * Avoid leaving a contract uninitialized.
 *
 * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
 * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
 * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * /// @custom:oz-upgrades-unsafe-allow constructor
 * constructor() {
 *     _disableInitializers();
 * }
 * ```
 * ====
 */
abstract contract Initializable {
    /**
     * @dev Storage of the initializable contract.
     *
     * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions
     * when using with upgradeable contracts.
     *
     * @custom:storage-location erc7201:openzeppelin.storage.Initializable
     */
    struct InitializableStorage {
        /**
         * @dev Indicates that the contract has been initialized.
         */
        uint64 _initialized;
        /**
         * @dev Indicates that the contract is in the process of being initialized.
         */
        bool _initializing;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;

    /**
     * @dev The contract is already initialized.
     */
    error InvalidInitialization();

    /**
     * @dev The contract is not initializing.
     */
    error NotInitializing();

    /**
     * @dev Triggered when the contract has been initialized or reinitialized.
     */
    event Initialized(uint64 version);

    /**
     * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
     * `onlyInitializing` functions can be used to initialize parent contracts.
     *
     * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any
     * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in
     * production.
     *
     * Emits an {Initialized} event.
     */
    modifier initializer() {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        // Cache values to avoid duplicated sloads
        bool isTopLevelCall = !$._initializing;
        uint64 initialized = $._initialized;

        // Allowed calls:
        // - initialSetup: the contract is not in the initializing state and no previous version was
        //                 initialized
        // - construction: the contract is initialized at version 1 (no reininitialization) and the
        //                 current contract is just being deployed
        bool initialSetup = initialized == 0 && isTopLevelCall;
        bool construction = initialized == 1 && address(this).code.length == 0;

        if (!initialSetup && !construction) {
            revert InvalidInitialization();
        }
        $._initialized = 1;
        if (isTopLevelCall) {
            $._initializing = true;
        }
        _;
        if (isTopLevelCall) {
            $._initializing = false;
            emit Initialized(1);
        }
    }

    /**
     * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
     * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
     * used to initialize parent contracts.
     *
     * A reinitializer may be used after the original initialization step. This is essential to configure modules that
     * are added through upgrades and that require initialization.
     *
     * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
     * cannot be nested. If one is invoked in the context of another, execution will revert.
     *
     * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
     * a contract, executing them in the right order is up to the developer or operator.
     *
     * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization.
     *
     * Emits an {Initialized} event.
     */
    modifier reinitializer(uint64 version) {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing || $._initialized >= version) {
            revert InvalidInitialization();
        }
        $._initialized = version;
        $._initializing = true;
        _;
        $._initializing = false;
        emit Initialized(version);
    }

    /**
     * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
     * {initializer} and {reinitializer} modifiers, directly or indirectly.
     */
    modifier onlyInitializing() {
        _checkInitializing();
        _;
    }

    /**
     * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
     */
    function _checkInitializing() internal view virtual {
        if (!_isInitializing()) {
            revert NotInitializing();
        }
    }

    /**
     * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
     * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
     * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
     * through proxies.
     *
     * Emits an {Initialized} event the first time it is successfully executed.
     */
    function _disableInitializers() internal virtual {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing) {
            revert InvalidInitialization();
        }
        if ($._initialized != type(uint64).max) {
            $._initialized = type(uint64).max;
            emit Initialized(type(uint64).max);
        }
    }

    /**
     * @dev Returns the highest version that has been initialized. See {reinitializer}.
     */
    function _getInitializedVersion() internal view returns (uint64) {
        return _getInitializableStorage()._initialized;
    }

    /**
     * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
     */
    function _isInitializing() internal view returns (bool) {
        return _getInitializableStorage()._initializing;
    }

    /**
     * @dev Returns a pointer to the storage namespace.
     */
    // solhint-disable-next-line var-name-mixedcase
    function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
        assembly {
            $.slot := INITIALIZABLE_STORAGE
        }
    }
}

// File: lib/liquidity-helper/UniswapV2LiquidityHelper.sol



// SPDX-License-Identifier: VPL
pragma solidity ^0.8.20;

import "solady/src/auth/Ownable.sol";
import "openzeppelin/token/ERC20/IERC20.sol";
import "solidity-lib/libraries/TransferHelper.sol";
import "v2-periphery/interfaces/IUniswapV2Router02.sol";
import "v2-periphery/interfaces/IWETH.sol";
import "./UniswapV2Library.sol";

// Majority of code was taken from the following link
// https://github.com/Roger-Wu/uniswap-v2-liquidity-adder-contract/blob/master/contracts/UniswapV2AddLiquidityHelperV1_1.sol
// Quite a few changes were necessary for Solidity >0.8.0 but most of the logic is still @Roger-Wu's

contract UniswapV2LiquidityHelper is Ownable {
    address public immutable _uniswapV2FactoryAddress;
    address public immutable _uniswapV2Router02Address;
    address public immutable _wethAddress;

    constructor(
        address uniswapV2FactoryAddress,
        address uniswapV2Router02Address,
        address wethAddress
    ) payable {
        _uniswapV2FactoryAddress = uniswapV2FactoryAddress;
        _uniswapV2Router02Address = uniswapV2Router02Address;
        _wethAddress = wethAddress;
        _initializeOwner(msg.sender);
    }

    // fallback() external payable {}
    receive() external payable {}

    // Add as more tokenA and tokenB as possible to a Uniswap pair.
    // The ratio between tokenA and tokenB can be any.
    // Approve enough amount of tokenA and tokenB to this contract before calling this function.
    // Uniswap pair tokenA-tokenB must exist.
    function swapAndAddLiquidityTokenAndToken(
        address tokenAddressA,
        address tokenAddressB,
        uint112 amountA,
        uint112 amountB,
        uint112 minLiquidityOut,
        address to
    ) external returns(uint liquidity) {
        require(amountA > 0 || amountB > 0, "amounts can not be both 0");

        // transfer user's tokens to this contract
        if (amountA > 0) {
            TransferHelper.safeTransferFrom(tokenAddressA, msg.sender, address(this), uint(amountA));
        }
        if (amountB > 0) {
            TransferHelper.safeTransferFrom(tokenAddressB, msg.sender, address(this), uint(amountB));
        }

        return _swapAndAddLiquidity(
            tokenAddressA,
            tokenAddressB,
            uint(amountA),
            uint(amountB),
            uint(minLiquidityOut),
            to
        );
    }

    // Add as more ether and tokenB as possible to a Uniswap pair.
    // The ratio between ether and tokenB can be any.
    // Approve enough amount of tokenB to this contract before calling this function.
    // Uniswap pair WETH-tokenB must exist.
    /*function swapAndAddLiquidityEthAndToken(
        address tokenAddressB,
        uint112 amountB,
        uint112 minLiquidityOut,
        address to
    ) external payable returns(uint liquidity) {
        uint amountA = msg.value;
        address tokenAddressA = _wethAddress;

        require(amountA > 0 || amountB > 0, "amounts can not be both 0");

        // convert ETH to WETH
        IWETH(_wethAddress).deposit{value: amountA}();
        // transfer user's tokenB to this contract
        if (amountB > 0) {
            TransferHelper.safeTransferFrom(tokenAddressB, msg.sender, address(this), uint(amountB));
        }

        return _swapAndAddLiquidity(
            tokenAddressA,
            tokenAddressB,
            amountA,
            uint(amountB),
            uint(minLiquidityOut),
            to
        );
    }*/

    // add as more tokens as possible to a Uniswap pair
    function _swapAndAddLiquidity(
        address tokenAddressA,
        address tokenAddressB,
        uint amountA,
        uint amountB,
        uint minLiquidityOut,
        address to
    ) internal returns(uint liquidity) {
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(_uniswapV2FactoryAddress, tokenAddressA, tokenAddressB);

        // Swap tokenA and tokenB s.t. amountA / reserveA >= amountB / reserveB
        // (or amountA * reserveB >= reserveA * amountB)
        // which means we will swap part of tokenA to tokenB before adding liquidity.
        if (amountA * reserveB < reserveA * amountB) {
            (tokenAddressA, tokenAddressB) = (tokenAddressB, tokenAddressA);
            (reserveA, reserveB) = (reserveB, reserveA);
            (amountA, amountB) = (amountB, amountA);
        }
        uint amountAToAdd = amountA;
        uint amountBToAdd = amountB;
        if (IERC20(tokenAddressA).allowance(address(this), _uniswapV2Router02Address) < amountA) {
            TransferHelper.safeApprove(tokenAddressA, _uniswapV2Router02Address, 2**256 - 1);
        }

        uint amountAToSwap = calcAmountAToSwap(reserveA, reserveB, amountA, amountB);
        require(amountAToSwap <= amountA, "bugs in calcAmountAToSwap cause amountAToSwap > amountA");
        if (amountAToSwap > 0) {
            address[] memory path = new address[](2);
            path[0] = tokenAddressA;
            path[1] = tokenAddressB;

            uint[] memory swapOutAmounts = IUniswapV2Router02(_uniswapV2Router02Address).swapExactTokensForTokens(
                amountAToSwap, // uint amountIn,
                1, // uint amountOutMin,
                path, // address[] calldata path,
                address(this), // address to,
                2**256-1 // uint deadline
            );

            amountAToAdd -= amountAToSwap;
            amountBToAdd += swapOutAmounts[swapOutAmounts.length - 1];
        }

        // _approveTokenToRouterIfNecessary(tokenAddressB, amountBToAdd);
        if (IERC20(tokenAddressB).allowance(address(this), _uniswapV2Router02Address) < amountBToAdd) {
            TransferHelper.safeApprove(tokenAddressB, _uniswapV2Router02Address, 2**256 - 1);
        }
        (, , liquidity) = IUniswapV2Router02(_uniswapV2Router02Address).addLiquidity(
            tokenAddressA, // address tokenA,
            tokenAddressB, // address tokenB,
            amountAToAdd, // uint amountADesired,
            amountBToAdd, // uint amountBDesired,
            1, // uint amountAMin,
            1, // uint amountBMin,
            to, // address to,
            2**256-1 // uint deadline
        );

        require(liquidity >= minLiquidityOut, "minted liquidity not enough");

        // Due to the inaccuracy of integer division,
        // there may be a small amount of tokens left in this contract.
        // Usually it doesn't worth it to spend more gas to transfer them out.
        // These tokens will be considered as a donation to the owner.
        // All ether and tokens directly sent to this contract will be considered as a donation to the contract owner.
    }

    function calcAmountAToSwap(
        uint reserveA,
        uint reserveB,
        uint amountA,
        uint amountB
    ) public pure returns(
        uint amountAToSwap
    ) {
        // separating requirements somehow saves gas.
        require(reserveA > 0, "reserveA can't be empty");
        require(reserveB > 0, "reserveB can't be empty");
        require(reserveA < 2**112, "reserveA must be < 2**112");
        require(reserveB < 2**112, "reserveB must be < 2**112");
        require(amountA < 2**112, "amountA must be < 2**112");
        require(amountB < 2**112, "amountB must be < 2**112");
        require(amountA * reserveB >= reserveA * amountB, "require amountA / amountB >= reserveA / reserveB");

        uint l = 0; // minAmountAToSwap
        uint r = amountA; // maxAmountAToSwap
        // avoid binary search going too deep. saving gas
        uint tolerance = amountA / 10000;
        if (tolerance == 0) { tolerance = 1; }
        uint newReserveA;
        uint newReserveB;
        uint newAmountA;
        uint newAmountB;

        // cache rA_times_1000 and rA_times_rB_times_1000 to save gas
        // Since reserveA, reserveB are both < 2**112,
        // rA_times_rB_times_1000 won't overflow.
        uint rA_times_1000 = reserveA * 1000;
        uint rA_times_rB_times_1000 = rA_times_1000 * reserveB;

        // goal:
        //   after swap l tokenA,
        //     newAmountA / newAmountB >= newReserveA / newReserveB
        //   after swap r tokenA,
        //     newAmountA / newAmountB < newReserveA / newReserveB
        //   r <= l + tolerance
        while (l + tolerance < r) {
            amountAToSwap = (l + r) / 2;

            newReserveA = reserveA + amountAToSwap;
            // (1000 * reserveA + 997 * amountAToSwap) * newReserveB = 1000 * reserveA * reserveB
            newReserveB = rA_times_rB_times_1000 / (rA_times_1000 + 997 * amountAToSwap);
            newAmountA = amountA - amountAToSwap; // amountAToSwap <= amountA
            newAmountB = amountB + (reserveB - newReserveB); // newReserveB <= reserveB
            if (newAmountA * newReserveB >= newReserveA * newAmountB) {
                l = amountAToSwap;
            } else {
                r = amountAToSwap;
            }
        }
        return l;
    }

    function emergencyWithdrawEther() external onlyOwner {
        (bool success, ) = payable(msg.sender).call{ value: address(this).balance }("");
        require(success, "withdraw failure");
    }

    function emergencyWithdrawErc20(address tokenAddress) external onlyOwner {
        IERC20 token = IERC20(tokenAddress);
        TransferHelper.safeTransfer(tokenAddress, msg.sender, token.balanceOf(address(this)));
    }
}
// File: lib/openzeppelin-contracts/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: lib/openzeppelin-contracts/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: lib/solidity-lib/contracts/libraries/TransferHelper.sol



// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.6.0;

// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
library TransferHelper {
    function safeApprove(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED');
    }

    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
    }

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');
    }

    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(success, 'TransferHelper: ETH_TRANSFER_FAILED');
    }
}

// File: lib/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol



pragma solidity >=0.6.2;

import './IUniswapV2Router01.sol';

interface IUniswapV2Router02 is IUniswapV2Router01 {
    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external returns (uint amountETH);
    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountETH);

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external;
    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external payable;
    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external;
}

// File: lib/v2-periphery/contracts/interfaces/IWETH.sol



pragma solidity >=0.5.0;

interface IWETH {
    function deposit() external payable;
    function transfer(address to, uint value) external returns (bool);
    function withdraw(uint) external;
}

// File: lib/liquidity-helper/UniswapV2Library.sol



pragma solidity >=0.5.0;

import 'v2-core/interfaces/IUniswapV2Pair.sol';

library UniswapV2Library {
    // returns sorted token addresses, used to handle return values from pairs sorted in this order
    function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
        require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
    }

    // calculates the CREATE2 address for a pair without making any external calls
    function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        pair = address(uint160(uint256(keccak256(abi.encodePacked(
                hex'ff',
                factory,
                keccak256(abi.encodePacked(token0, token1)),
                hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303' // SushiSwap init code hash
            )))));
    }

    // fetches and sorts the reserves for a pair
    function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
        (address token0,) = sortTokens(tokenA, tokenB);
        (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }
}
// File: lib/openzeppelin-contracts/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);
}

// File: lib/v2-periphery/contracts/interfaces/IUniswapV2Router01.sol



pragma solidity >=0.6.2;

interface IUniswapV2Router01 {
    function factory() external pure returns (address);
    function WETH() external pure returns (address);

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB, uint liquidity);
    function addLiquidityETH(
        address token,
        uint amountTokenDesired,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external payable returns (uint amountToken, uint amountETH, uint liquidity);
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external returns (uint amountA, uint amountB);
    function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external returns (uint amountToken, uint amountETH);
    function removeLiquidityWithPermit(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountA, uint amountB);
    function removeLiquidityETHWithPermit(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external returns (uint amountToken, uint amountETH);
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
    function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        payable
        returns (uint[] memory amounts);
    function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
        external
        returns (uint[] memory amounts);
    function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        returns (uint[] memory amounts);
    function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
        external
        payable
        returns (uint[] memory amounts);

    function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
    function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
    function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts);
}

// File: lib/v2-core/contracts/interfaces/IUniswapV2Pair.sol



pragma solidity >=0.5.0;

interface IUniswapV2Pair {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
    function decimals() external pure returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);

    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function PERMIT_TYPEHASH() external pure returns (bytes32);
    function nonces(address owner) external view returns (uint);

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);

    function MINIMUM_LIQUIDITY() external pure returns (uint);
    function factory() external view returns (address);
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function price0CumulativeLast() external view returns (uint);
    function price1CumulativeLast() external view returns (uint);
    function kLast() external view returns (uint);

    function mint(address to) external returns (uint liquidity);
    function burn(address to) external returns (uint amount0, uint amount1);
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
    function skim(address to) external;
    function sync() external;

    function initialize(address, address) external;
}