// File: contracts/Adapter.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

/**
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

import "./AdapterHelper.sol";

contract Adapter is AdapterHelper {

    constructor(
        address _tokenProfitAddress,
        address _uniV2RouterAddress,
        address _liquidNFTsRouterAddress,
        address _liquidNFTsWETHPool,
        address _liquidNFTsUSDCPool,
        address _wiseLendingAddress,
        bool _isPaused
    )
        AdapterDeclarations(
            _tokenProfitAddress,
            _uniV2RouterAddress,
            _liquidNFTsRouterAddress,
            _liquidNFTsWETHPool,
            _liquidNFTsUSDCPool,
            _wiseLendingAddress,
            _isPaused
        )
    {
        admin = tx.origin;
        multisig = tx.origin;
    }

    /**
    * @dev Overview for services and ability to syncronize them
    */
    function syncServices()
        external
    {
        liquidNFTsUSDCPool.manualSyncPool();
        liquidNFTsWETHPool.manualSyncPool();
    }

    /**
    * @dev Allows admin to swap USDC for ETH using UniswapV2
    */
    function swapUSDCForETH(
        uint256 _amount,
        uint256 _minAmountOut
    )
        external
        onlyMultiSig
        returns (
            uint256 amountIn,
            uint256 amountOut
        )
    {
        address[] memory path = new address[](2);

        path[0] = USDC_ADDRESS;
        path[1] = WETH_ADDRESS;

        uint256[] memory amounts = _executeSwap(
            path,
            _amount,
            _minAmountOut
        );

        emit AdminSwap(
            USDC_ADDRESS,
            WETH_ADDRESS,
            amounts[0],
            amounts[1]
        );

        return (
            amounts[0],
            amounts[1]
        );
    }

    function _precisionWithFee()
        internal
        view
        returns (uint256)
    {
        return FEE_PRECISION + buyFee;
    }

    /**
     * @dev Calculates tokens to mint given exact ETH amount
     * input for the TokenProfit contract during mint
     */
    function getTokenAmountFromEthAmount(
        uint256 _ethAmount
    )
        external
        view
        returns (uint256)
    {
        return tokenProfit.totalSupply()
            * _ethAmount
            * FEE_PRECISION
            / _precisionWithFee()
            / _calculateTotalEthValue(
                0
            );
    }

    /**
     * @dev Calculates ETH amount necessary to pay to
     * receive fixed token amount from the TokenProfit contract
     */
    function getEthAmountFromTokenAmount(
        uint256 _tokenAmount,
        uint256 _msgValue
    )
        external
        view
        returns (uint256)
    {
        return _calculateTotalEthValue(
            _msgValue
        )
            * _tokenAmount
            * PRECISION_FACTOR
            * _precisionWithFee()
            / FEE_PRECISION
            / tokenProfit.totalSupply()
            / PRECISION_FACTOR;
    }

    function proposeNewMultisig(
        address _proposedMultisig
    )
        external
        onlyMultiSig
    {
        address oldProposedMultisig = proposedMultisig;
        proposedMultisig = _proposedMultisig;

        emit MultisigUpdateProposed(
            oldProposedMultisig,
            _proposedMultisig
        );
    }

    function claimMultisigOwnership()
        external
        onlyProposedMultisig
    {
        address oldMultisig = multisig;
        multisig = proposedMultisig;
        proposedMultisig = ZERO_ADDRESS;

        emit MultisigUpdate(
            oldMultisig,
            multisig
        );
    }

    /**
     * @dev Starts the process to change the admin address
     */
    function proposeNewAdmin(
        address _newProposedAdmin
    )
        external
        onlyMultiSig
    {
        address oldProposedAdmin = proposedAdmin;
        proposedAdmin = _newProposedAdmin;

        emit AdminUpdateProposed(
            oldProposedAdmin,
            _newProposedAdmin
        );
    }

    /**
     * @dev Finalize the change of the admin address
     */
    function claimAdminOwnership()
        external
        onlyProposedAdmin
    {
        address oldAdmin = admin;
        admin = proposedAdmin;
        proposedAdmin = ZERO_ADDRESS;

        emit AdminUpdate(
            oldAdmin,
            admin
        );
    }

    /**
     * @dev Ability for multisig to change the buy fee
     */
    function changeBuyFee(
        uint256 _newFeeValue
    )
        external
        onlyMultiSig
    {
        if (_newFeeValue > FEE_THRESHOLD) {
            revert ValueTooHigh();
        }

        if (_newFeeValue < FEE_LOWER_BOUND) {
            revert ValueTooLow();
        }

        uint256 oldValue = buyFee;
        buyFee = _newFeeValue;

        emit BuyFeeChanged(
            oldValue,
            _newFeeValue
        );
    }

    /**
     * @dev Allows multisig to swap ETH for USDC using UniswapV2
     */
    function swapETHforUSDC(
        uint256 _amount,
        uint256 _minAmountOut
    )
        external
        onlyMultiSig
        returns (
            uint256 amountIn,
            uint256 amountOut
        )
    {
        address[] memory path = new address[](2);

        path[0] = WETH_ADDRESS;
        path[1] = USDC_ADDRESS;

        uint256[] memory amounts = _executeSwapWithValue(
            path,
            _amount,
            _minAmountOut
        );

        emit AdminSwap(
            WETH_ADDRESS,
            USDC_ADDRESS,
            amounts[0],
            amounts[1]
        );

        return (
            amounts[0],
            amounts[1]
        );
    }

    /**
     * @dev Allows admin to deposit funds into WiseLending protocol
     */
    function depositWiseLending(
        address _poolToken,
        uint256 _depositAmount
    )
        external
        onlyAdmin
    {
        if (_poolToken == WETH_ADDRESS) {
            _wrapETH(
                _depositAmount
            );
        }

        IERC20(_poolToken).approve(
            address(WISE_LENDING),
            _depositAmount
        );

        _depositWiseLendingWrapper(
            _poolToken,
            _depositAmount
        );
    }

    /**
     * @dev Allows admin to deposit funds into WiseLending protocol
     */
    function withdrawWiseLending(
        address _poolToken,
        uint256 _withdrawAmount
    )
        external
        onlyAdmin
    {
        _withdrawWiseLendingWrapper(
            _poolToken,
            _withdrawAmount
        );

        if (_poolToken == WETH_ADDRESS) {
            _unwrapETH(
                _withdrawAmount
            );
        }
    }

    /**
     * @dev Allows admin to deposit ETH into LiquidNFTs pool
     */
    function depositETHLiquidNFTs(
        uint256 _amount
    )
        external
        onlyAdmin
    {
        _wrapETH(
            _amount
        );

        _depositLiquidNFTsWrapper(
            liquidNFTsWETHPool,
            _amount
        );
    }

    /**
     * @dev Allows admin to deposit USDC into LiquidNFTs pool
     */
    function depositUSDCLiquidNFTs(
        uint256 _amount
    )
        external
        onlyAdmin
    {
        _depositLiquidNFTsWrapper(
            liquidNFTsUSDCPool,
            _amount
        );
    }

    /**
     * @dev Allows admin to withdraw USDC from LiquidNFTs pool
     */
    function withdrawUSDCLiquidNFTs(
        uint256 _amount
    )
        external
        onlyAdmin
    {
        _withdrawLiquidNFTsWrapper(
            liquidNFTsUSDCPool,
            _amount
        );
    }

    /**
     * @dev Allows admin to withdraw ETH from LiquidNFTs pool
     */
    function withdrawETHLiquidNFTs(
        uint256 _amount
    )
        external
        onlyAdmin
    {
        _withdrawLiquidNFTsWrapper(
            liquidNFTsWETHPool,
            _amount
        );

        _unwrapETH(
            WETH.balanceOf(
                TOKEN_PROFIT_ADDRESS
            )
        );
    }

    /**
     * @dev Allows TokenProfit contract to withdraw tokens from services
     */
    function assistWithdrawTokens(
        uint256 _index,
        uint256 _amount
    )
        external
        onlyTokenProfit
        returns (uint256)
    {
        if (tokens[_index].tokenERC20 == USDC) {
            return _USDCRoutine(
                _amount
            );
        }

        revert InvalidToken();
    }

    /**
     * @dev Allows TokenProfit contract to withdraw ETH from services
     */
    function assistWithdrawETH(
        uint256 _amount
    )
        external
        onlyTokenProfit
        returns (uint256)
    {
        uint256 wethAmountReturned = _WETHRoutine(
            _amount
        );

        _unwrapETH(
            wethAmountReturned
        );

        return wethAmountReturned;
    }

    /**
     * @dev Allows admin to forward activeNFT to a new address
     */
    function transferNFT(
        address _newAddress
    )
        external
        onlyAdmin
    {
        POSITION_NFTS.transferFrom(
            address(this),
            _newAddress,
            ACTIVE_NFT
        );

        emit NFTForwarded(
            ACTIVE_NFT,
            _newAddress
        );
    }
}

// File: contracts/AdapterHelper.sol



// SPDX-License-Identifier: --DAO--

/**
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

pragma solidity =0.8.21;

import "./AdapterDeclarations.sol";

error SlippageTooBig();
error ChainLinkOffline();

abstract contract AdapterHelper is AdapterDeclarations {

    /**
    * @dev Tells TokenProfit contract to perform a swap through UniswapV2 starting with ETH
    */
    function _executeSwapWithValue(
        address[] memory _path,
        uint256 _amount,
        uint256 _minAmountOut
    )
        internal
        returns (uint256[] memory)
    {
        if (_minAmountOut == 0) {
            revert SlippageTooBig();
        }

        bytes memory callbackData = tokenProfit.executeAdapterRequestWithValue(
            UNIV2_ROUTER_ADDRESS,
            abi.encodeWithSelector(
                IUniswapV2.swapExactETHForTokens.selector,
                _minAmountOut,
                _path,
                TOKEN_PROFIT_ADDRESS,
                block.timestamp
            ),
            _amount
        );

        return abi.decode(
            callbackData,
            (
                uint256[]
            )
        );
    }

    /**
    * @dev checks if chainLink price feeds are still operating
    */
    function isChainlinkOffline()
        public
        view
        returns (bool)
    {
        for (uint256 i = 0; i < TOKENS; i++) {

            IChainLink feed = tokens[i].feedLink;

            (   ,
                ,
                ,
                uint256 upd
                ,
            ) = feed.latestRoundData();

            upd = block.timestamp > upd
                ? block.timestamp - upd
                : block.timestamp;

            if (upd > chainLinkHeartBeat[address(feed)]) return true;
        }

        return false;
    }

    /**
    * @dev Calculates ETH and token balances available for services
    * --------------------------------
    * availableEther is ETH balance of the TokenProfit contract
    * availableTokens is balances of all tokens in TokenProfit contract
    * etherAmount is availableEther + ETH deposited in other services
    * tokenAmounts is availableTokens + tokens deposited in other services
    */
    function getTokenAmounts()
        public
        view
        returns (
            uint256 etherAmount,
            uint256[] memory tokensAmounts,
            uint256 availableEther,
            uint256[] memory availableAmounts
        )
    {
        uint256[] memory tokenAmounts = new uint256[](
            TOKENS
        );

        uint256[] memory availableTokens = new uint256[](
            TOKENS
        );

        (
            availableEther,
            availableTokens
        ) = _getAvailableFunds();

        for (uint256 i = 0; i < TOKENS; i++) {
            tokenAmounts[i] = _getReservesByToken(
                tokens[i].tokenERC20
            ) + availableTokens[i];
        }

        etherAmount = _depositedInLiquidNFTs(
            liquidNFTsWETHPool,
            TOKEN_PROFIT_ADDRESS
        )
            + availableEther
            + _depositedInWiseLending(
                WETH_ADDRESS
            );

        return (
            etherAmount,
            tokenAmounts,
            availableEther,
            availableTokens
        );
    }

    function _calculateTotalEthValue(
        uint256 _msgValue
    )
        internal
        view
        returns (uint256)
    {
        if (isChainlinkOffline() == true) {
            revert ChainLinkOffline();
        }

        (
            uint256 etherAmount,
            uint256[] memory tokensAmounts,
            ,
        ) = getTokenAmounts();

        for (uint256 i = 0; i < TOKENS; i++) {

            TokenData memory token = tokens[i];
            IChainLink feed = token.feedLink;

            uint256 latestAnswer = feed.latestAnswer();

            require(
                latestAnswer > 0,
                "AdapterHelper: CHAINLINK_OFFLINE"
            );

            etherAmount += feed.latestAnswer()
                * PRECISION_FACTOR
                * tokensAmounts[i]
                / (10 ** token.tokenDecimals)
                / (10 ** token.feedDecimals);
        }

        return etherAmount
            - _msgValue;
    }

    /**
    * @dev Tells TokenProfit contract to perform
    * a swap through UniswapV2 starting with Tokens
    */
    function _executeSwap(
        address[] memory _path,
        uint256 _amount,
        uint256 _minAmountOut
    )
        internal
        returns (uint256[] memory)
    {
        if (_minAmountOut == 0) {
            revert SlippageTooBig();
        }

        bytes memory callbackData = tokenProfit.executeAdapterRequest(
            UNIV2_ROUTER_ADDRESS,
            abi.encodeWithSelector(
                IUniswapV2.swapExactTokensForETH.selector,
                _amount,
                _minAmountOut,
                _path,
                TOKEN_PROFIT_ADDRESS,
                block.timestamp
            )
        );

        return abi.decode(
            callbackData,
            (
                uint256[]
            )
        );
    }

    /**
    * @dev Tells TokenProfit contract to convert WETH to ETH
    */
    function _unwrapETH(
        uint256 _amount
    )
        internal
    {
        tokenProfit.executeAdapterRequest(
            WETH_ADDRESS,
            abi.encodeWithSelector(
                IWETH.withdraw.selector,
                _amount
            )
        );
    }

    /**
    * @dev Tells TokenProfit contract to convert ETH to WETH
    */
    function _wrapETH(
        uint256 _amount
    )
        internal
    {
        tokenProfit.executeAdapterRequestWithValue(
            WETH_ADDRESS,
            abi.encodeWithSelector(
                IWETH.deposit.selector
            ),
            _amount
        );
    }

    /**
    * @dev Tells TokenProfit contract to deposit funds into WiseLending
    */
    function _depositWiseLendingWrapper(
        address _poolToken,
        uint256 _depositAmount
    )
        internal
    {
        tokenProfit.executeAdapterRequest(
            _poolToken,
            abi.encodeWithSelector(
                IERC20.transfer.selector,
                address(this),
                _depositAmount
            )
        );

        WISE_LENDING.depositExactAmount(
            ACTIVE_NFT,
            _poolToken,
            _depositAmount
        );
    }

    /**
    * @dev Tells TokenProfit contract to deposit funds into WiseLending
    */
    function _withdrawWiseLendingWrapper(
        address _poolToken,
        uint256 _withdrawAmount
    )
        internal
    {
        if (_withdrawAmount == 0) {
            return;
        }

        WISE_LENDING.withdrawExactAmount(
            ACTIVE_NFT,
            _poolToken,
            _withdrawAmount
        );

        IERC20(_poolToken).transfer(
            TOKEN_PROFIT_ADDRESS,
            _withdrawAmount
        );
    }

    /**
    * @dev Tells TokenProfit contract to deposit funds into LiquidNFTs pool
    */
    function _depositLiquidNFTsWrapper(
        ILiquidNFTsPool _pool,
        uint256 _amount
    )
        internal
    {
        tokenProfit.executeAdapterRequest(
            LIQUID_NFT_ROUTER_ADDRESS,
            abi.encodeWithSelector(
                ILiquidNFTsRouter.depositFunds.selector,
                _amount,
                _pool
            )
        );
    }

    /**
    * @dev Tells TokenProfit contract to withdraw funds from LiquidNFTs pool
    */
    function _withdrawLiquidNFTsWrapper(
        ILiquidNFTsPool _pool,
        uint256 _amount
    )
        internal
    {
        tokenProfit.executeAdapterRequest(
            LIQUID_NFT_ROUTER_ADDRESS,
            abi.encodeWithSelector(
                ILiquidNFTsRouter.withdrawFunds.selector,
                _calculateSharesFromAmount(
                    _pool,
                    _amount
                ),
                _pool
            )
        );
    }

    /**
    * @dev Routine used to deal with all services withdrawing USDC
    */
    function _USDCRoutine(
        uint256 _amount
    )
        internal
        returns (uint256)
    {
        uint256 balanceBefore = USDC.balanceOf(
            TOKEN_PROFIT_ADDRESS
        );

        (
            uint256 withdrawnAmountWiseLending
        ) = _withdrawFromWiseLending(
            _amount,
            USDC_ADDRESS
        );

        if (withdrawnAmountWiseLending == _amount) {
            return _amount;
        }

        _withdrawLiquidNFTsWrapper(
            liquidNFTsUSDCPool,
            _amount - withdrawnAmountWiseLending
        );

        uint256 balanceAfter = USDC.balanceOf(
            TOKEN_PROFIT_ADDRESS
        );

        return balanceAfter - balanceBefore;
    }

    /**
    * @dev Routine used to deal with all services withdrawing ETH
    */
    function _WETHRoutine(
        uint256 _amount
    )
        internal
        returns (uint256)
    {
        (
            uint256 withdrawnAmountWiseLending
        ) = _withdrawFromWiseLending(
            _amount,
            WETH_ADDRESS
        );

        if (withdrawnAmountWiseLending == _amount) {
            return _amount;
        }

        _withdrawLiquidNFTsWrapper(
            liquidNFTsWETHPool,
            _amount - withdrawnAmountWiseLending
        );

        uint256 balance = WETH.balanceOf(
            TOKEN_PROFIT_ADDRESS
        );

        return balance;
    }

    function _withdrawFromWiseLending(
        uint256 _amount,
        address _poolToken
    )
        internal
        returns (uint256)
    {
        uint256 maxWithdrawableAmount = WISE_SECURITY.maximumWithdrawToken(
            ACTIVE_NFT,
            _poolToken,
            ZERO_INTERVAL,
            SOLELY_WITHDRAW_AMOUNT
        );

        uint256 withdrawnAmountWiseLending = maxWithdrawableAmount >= _amount
            ? _amount
            : maxWithdrawableAmount;

        _withdrawWiseLendingWrapper(
            _poolToken,
            withdrawnAmountWiseLending
        );

        return withdrawnAmountWiseLending;
    }

    /**
    * @dev Returns balances of TokenProfit contract - tokens and ETH
    */
    function _getAvailableFunds()
        internal
        view
        returns (
            uint256,
            uint256[] memory
        )
    {
        uint256[] memory availableTokens = new uint256[](
            TOKENS
        );

        for (uint256 i = 0; i < TOKENS; i++) {
            IERC20 token = tokens[i].tokenERC20;
            availableTokens[i] = token.balanceOf(
                TOKEN_PROFIT_ADDRESS
            );
        }

        uint256 availableEther = TOKEN_PROFIT_ADDRESS.balance;

        return (
            availableEther,
            availableTokens
        );
    }

    /**
    * @dev Returns balances locked in servcies based on token
    */
    function _getReservesByToken(
        IERC20 _token
    )
        internal
        view
        returns (uint256)
    {
        if (_token == USDC) {
            return _depositedInLiquidNFTs(
                liquidNFTsUSDCPool,
                TOKEN_PROFIT_ADDRESS
            ) + _depositedInWiseLending(
                USDC_ADDRESS
            );
        }

        return 0;
    }

    /**
    * @dev Helper function to calculate shares from amount for LiquidNFTs pool
    */
    function _calculateSharesFromAmount(
        ILiquidNFTsPool _pool,
        uint256 _amount
    )
        internal
        view
        returns (uint256)
    {
        return _amountSharesCalculationWrapper(
            _pool.totalInternalShares(),
            _pool.pseudoTotalTokensHeld(),
            _amount
        );
    }

    uint256 private constant DEPOSITED_IN_LIQUID_NFTS = 280 * 1E18;
    uint256 private constant DEPOSITED_IN_WISE_LENDING = 110 * 1E18;

    /**
    * @dev Helper function to calculate amount from shares for LiquidNFTs pool
    */
    function _depositedInLiquidNFTs(
        ILiquidNFTsPool _pool,
        address _sharesHolder
    )
        internal
        view
        returns (uint256)
    {
        if (PAUSE_GROW == true && _pool == liquidNFTsWETHPool) {
            return DEPOSITED_IN_LIQUID_NFTS;
        }

        return _amountSharesCalculationWrapper(
            _pool.pseudoTotalTokensHeld(),
            _pool.totalInternalShares(),
            _pool.internalShares(
                _sharesHolder
            )
        );
    }

    /**
    * @dev Helper function to calculate amount deposited into WiseLending
    */
    function _depositedInWiseLending(
        address _poolToken
    )
        internal
        view
        returns (uint256)
    {
        uint256 depositedBefore;

        if (PAUSE_GROW == true && _poolToken == WETH_ADDRESS) {
            depositedBefore = DEPOSITED_IN_WISE_LENDING;
        }

        return WISE_SECURITY.getPositionLendingAmount(
            ACTIVE_NFT,
            _poolToken
        ) + depositedBefore;
    }

    /**
    * @dev Calculates ratios based on shares and amount
    */
    function _amountSharesCalculationWrapper(
        uint256 _totalValue,
        uint256 _correspondingTotalValue,
        uint256 _amountValue
    )
        internal
        pure
        returns (uint256)
    {
        return _totalValue
            * _amountValue
            / _correspondingTotalValue;
    }
}

// File: contracts/AdapterDeclarations.sol



// SPDX-License-Identifier: -- DAO --

/**
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

pragma solidity =0.8.21;

import "./AdapterInterfaces.sol";

error InvalidNFT();
error InvalidFeed();
error InvalidToken();
error InvalidDecimals();

error ValueTooLow();
error ValueTooHigh();

contract AdapterDeclarations {

    struct TokenData {
        IERC20 tokenERC20;
        IChainLink feedLink;
        uint8 feedDecimals;
        uint8 tokenDecimals;
    }

    uint256 constant TOKENS = 1;
    uint256 constant ZERO_INTERVAL = 0;
    uint256 constant SOLELY_WITHDRAW_AMOUNT = 0;

    TokenData[TOKENS] public tokens;

    bool public immutable PAUSE_GROW;
    uint256 public immutable ACTIVE_NFT;

    IERC20 public immutable WETH;
    IERC20 public immutable USDC;

    address public immutable WETH_ADDRESS;
    address public immutable USDC_ADDRESS;

    address public immutable TOKEN_PROFIT_ADDRESS;
    address public immutable UNIV2_ROUTER_ADDRESS;
    address public immutable LIQUID_NFT_ROUTER_ADDRESS;

    IWiseLending public immutable WISE_LENDING;
    IWiseSecurity public immutable WISE_SECURITY;
    IPositionNFTs public immutable POSITION_NFTS;

    ITokenProfit public immutable tokenProfit;
    ILiquidNFTsPool public immutable liquidNFTsWETHPool;
    ILiquidNFTsPool public immutable liquidNFTsUSDCPool;

    address public admin;
    address public multisig;
    address public proposedMultisig;
    address public proposedAdmin;

    uint256 public buyFee = 1000;

    uint256 public constant FEE_PRECISION = 1E4;
    uint256 public constant FEE_THRESHOLD = 50000;
    uint256 public constant FEE_LOWER_BOUND = 10;
    uint256 public constant PRECISION_FACTOR = 1E18;

    uint80 constant MAX_ROUND_COUNT = 50;
    address constant ZERO_ADDRESS = address(0x0);
    uint256 constant UINT256_MAX = type(uint256).max;

    mapping(address => uint256) public chainLinkHeartBeat;

    modifier onlyAdmin() {
        require(
            msg.sender == admin,
            "AdapterDeclarations: NOT_ADMIN"
        );
        _;
    }

    modifier onlyTokenProfit() {
        require(
            msg.sender == TOKEN_PROFIT_ADDRESS,
            "AdapterDeclarations: NOT_TOKEN_PROFIT"
        );
        _;
    }

    modifier onlyProposedMultisig() {
        require(
            msg.sender == proposedMultisig,
            "AdapterDeclarations: NOT_PROPOSED_MULTISIG"
        );
        _;
    }

    modifier onlyProposedAdmin() {
        require(
            msg.sender == proposedAdmin,
            "AdapterDeclarations: NOT_PROPOSED_ADMIN"
        );
        _;
    }

    modifier onlyMultiSig() {
        require(
            msg.sender == multisig,
            "AdapterDeclarations: NOT_MULTISIG"
        );
        _;
    }

    event AdminSwap(
        address indexed from,
        address indexed to,
        uint256 amountIn,
        uint256 amountOut
    );

    event BuyFeeChanged(
        uint256 indexed oldFee,
        uint256 indexed newFee
    );

    event MultisigUpdate(
        address oldMultisig,
        address newMultisig
    );

    event MultisigUpdateProposed(
        address oldProposedMultisig,
        address newProposedMultisig
    );

    event AdminUpdate(
        address oldAdmin,
        address newAdmin
    );

    event AdminUpdateProposed(
        address oldProposedAdmin,
        address newProposedAdmin
    );

    event NFTForwarded(
        uint256 nftId,
        address to
    );

    constructor(
        address _tokenProfitAddress,
        address _uniV2RouterAddress,
        address _liquidNFTsRouterAddress,
        address _liquidNFTsWETHPool,
        address _liquidNFTsUSDCPool,
        address _wiseLendingAddress,
        bool _isPaused
    ) {
        // --- liquidNFTs group ---

        liquidNFTsWETHPool = ILiquidNFTsPool(
            _liquidNFTsWETHPool
        );

        liquidNFTsUSDCPool = ILiquidNFTsPool(
            _liquidNFTsUSDCPool
        );

        LIQUID_NFT_ROUTER_ADDRESS = _liquidNFTsRouterAddress;

        // --- token group ---

        USDC_ADDRESS = liquidNFTsUSDCPool.poolToken();
        WETH_ADDRESS = liquidNFTsWETHPool.poolToken();

        USDC = IERC20(
            USDC_ADDRESS
        );

        WETH = IWETH(
            WETH_ADDRESS
        );

        IChainLink chainLinkFeed = IChainLink(
            0x986b5E1e1755e3C2440e960477f25201B0a8bbD4
        );

        WISE_LENDING = IWiseLending(
            _wiseLendingAddress
        );

        POSITION_NFTS = IPositionNFTs(
            WISE_LENDING.POSITION_NFT()
        );

        ACTIVE_NFT = POSITION_NFTS.mintPosition();

        WISE_SECURITY = IWiseSecurity(
            WISE_LENDING.WISE_SECURITY()
        );

        tokens[0] = TokenData({
            tokenERC20: USDC,
            feedLink: chainLinkFeed,
            feedDecimals: chainLinkFeed.decimals(),
            tokenDecimals: USDC.decimals()
        });

        _validateData();

        // --- tokenProfit group ---

        tokenProfit = ITokenProfit(
            _tokenProfitAddress
        );

        TOKEN_PROFIT_ADDRESS = _tokenProfitAddress;
        UNIV2_ROUTER_ADDRESS = _uniV2RouterAddress;

        PAUSE_GROW = _isPaused;
    }

    event ERC721Received(
        address _operator,
        address _from,
        uint256 _tokenId,
        bytes _data
    );

    function onERC721Received(
        address _operator,
        address _from,
        uint256 _tokenId,
        bytes calldata _data
    )
        external
        returns (bytes4)
    {
        emit ERC721Received(
            _operator,
            _from,
            _tokenId,
            _data
        );

        return this.onERC721Received.selector;
    }

    function validateData()
        external
    {
        _validateData();
    }

    function _validateData()
        private
    {
        for (uint256 i = 0; i < TOKENS; i++) {

            TokenData memory token = tokens[i];

            if (token.tokenDecimals == 0) {
                revert InvalidDecimals();
            }

            if (token.feedDecimals == 0) {
                revert InvalidDecimals();
            }

            if (token.tokenERC20 == IERC20(ZERO_ADDRESS)) {
                revert InvalidToken();
            }

            if (token.feedLink == IChainLink(ZERO_ADDRESS)) {
                revert InvalidFeed();
            }

            string memory expectedFeedName = string.concat(
                token.tokenERC20.symbol(),
                " / "
                "ETH"
            );

            string memory chainLinkFeedName = token.feedLink.description();

            require(
                keccak256(abi.encodePacked(expectedFeedName)) ==
                keccak256(abi.encodePacked(chainLinkFeedName)),
                "AdapterDeclarations: INVALID_CHAINLINK_FEED"
            );

            recalibrate(
                address(token.feedLink)
            );
        }
    }

    /**
     * @dev Determines info for the heartbeat update
     *  mechanism for chainlink oracles (roundIds)
     */
    function getLatestAggregatorRoundId(
        IChainLink _feed
    )
        public
        view
        returns (uint80)
    {
        (
            uint80 roundId,
            ,
            ,
            ,
        ) = _feed.latestRoundData();

        return uint64(roundId);
    }

    /**
     * @dev Determines number of iterations necessary during recalibrating
     * heartbeat.
     */
    function _getIterationCount(
        uint80 _latestAggregatorRoundId
    )
        internal
        pure
        returns (uint80)
    {
        return _latestAggregatorRoundId > MAX_ROUND_COUNT
            ? MAX_ROUND_COUNT
            : _latestAggregatorRoundId;
    }

    /**
     * @dev fetches timestamp of a byteshifted aggregatorRound with specific
     * phaseID. For more info see chainlink historical price data documentation
     */
    function _getRoundTimestamp(
        IChainLink _feed,
        uint16 _phaseId,
        uint80 _aggregatorRoundId
    )
        internal
        view
        returns (uint256)
    {
        (
            ,
            ,
            ,
            uint256 timestamp,
        ) = _feed.getRoundData(
            getRoundIdByByteShift(
                _phaseId,
                _aggregatorRoundId
            )
        );

        return timestamp;
    }

    /**
     * @dev Determines info for the heartbeat update mechanism for chainlink
     * oracles (shifted round Ids)
     */
    function getRoundIdByByteShift(
        uint16 _phaseId,
        uint80 _aggregatorRoundId
    )
        public
        pure
        returns (uint80)
    {
        return uint80(uint256(_phaseId) << 64 | _aggregatorRoundId);
    }

    /**
     * @dev Function to recalibrate the heartbeat for a specific feed
     */
    function recalibrate(
        address _feed
    )
        public
    {
        chainLinkHeartBeat[_feed] = recalibratePreview(
            IChainLink(_feed)
        );
    }

    /**
    * @dev View function to determine the heartbeat for a specific feed
    * Looks at the maximal last 50 rounds and takes second highest value to
    * avoid counting offline time of chainlink as valid heartbeat
    */
    function recalibratePreview(
        IChainLink _feed
    )
        public
        view
        returns (uint256)
    {
        uint80 latestAggregatorRoundId = getLatestAggregatorRoundId(
            _feed
        );

        uint80 iterationCount = _getIterationCount(
            latestAggregatorRoundId
        );

        if (iterationCount < 2) {
            revert("Adapter: SMALL_SAMPLE");
        }

        uint16 phaseId = _feed.phaseId();
        uint256 latestTimestamp = _getRoundTimestamp(
            _feed,
            phaseId,
            latestAggregatorRoundId
        );

        uint256 currentDiff;
        uint256 currentBiggest;
        uint256 currentSecondBiggest;

        for (uint80 i = 1; i < iterationCount; i++) {

            uint256 currentTimestamp = _getRoundTimestamp(
                _feed,
                phaseId,
                latestAggregatorRoundId - i
            );

            currentDiff = latestTimestamp - currentTimestamp;

            latestTimestamp = currentTimestamp;

            if (currentDiff >= currentBiggest) {
                currentSecondBiggest = currentBiggest;
                currentBiggest = currentDiff;
            } else if (currentDiff > currentSecondBiggest && currentDiff < currentBiggest) {
                currentSecondBiggest = currentDiff;
            }
        }

        return currentSecondBiggest;
    }

    function setApprovals()
        external
    {
        address[2] memory spenders = [
            UNIV2_ROUTER_ADDRESS,
            LIQUID_NFT_ROUTER_ADDRESS
        ];

        for (uint256 i = 0; i < spenders.length; i++) {
            for (uint256 j = 0; j < tokens.length; j++) {
                tokenProfit.executeAdapterRequest(
                    address(tokens[j].tokenERC20),
                    abi.encodeWithSelector(
                        IERC20.approve.selector,
                        spenders[i],
                        UINT256_MAX
                    )
                );
            }
            tokenProfit.executeAdapterRequest(
                WETH_ADDRESS,
                abi.encodeWithSelector(
                    IERC20.approve.selector,
                    spenders[i],
                    UINT256_MAX
                )
            );
        }
    }
}

// File: contracts/AdapterInterfaces.sol



// SPDX-License-Identifier: --DAO--

/**
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

pragma solidity =0.8.21;

import "./IERC20.sol";

interface IChainLink {

    function decimals()
        external
        view
        returns (uint8);

    function latestAnswer()
        external
        view
        returns (uint256);

    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answerdInRound
        );

    function getRoundData(
        uint80 _roundId
    )
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );

    function phaseId()
        external
        view
        returns(
            uint16 phaseId
        );

    function aggregator()
        external
        view
        returns (address);

    function description()
        external
        view
        returns (string memory);
}

interface ITokenProfit {

    function redeemRewards(
        uint256 _burnAmount
    )
        external
        returns (
            uint256,
            uint256[] memory
        );

    function changeAdapter(
        address _newAdapter
    )
        external;

    function getAvailableMint()
        external
        view
        returns (uint256);

    function executeAdapterRequest(
        address _contractToCall,
        bytes memory _callBytes
    )
        external
        returns (bytes memory);

    function executeAdapterRequestWithValue(
        address _contractToCall,
        bytes memory _callBytes,
        uint256 _value
    )
        external
        returns (bytes memory);

    function totalSupply()
        external
        view
        returns (uint256);

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

interface ILiquidNFTsRouter {

    function depositFunds(
        uint256 _amount,
        address _pool
    )
        external;

    function withdrawFunds(
        uint256 _amount,
        address _pool
    )
        external;
}

interface ILiquidNFTsPool {

    function pseudoTotalTokensHeld()
        external
        view
        returns (uint256);

    function totalInternalShares()
        external
        view
        returns (uint256);

    function manualSyncPool()
        external;

    function internalShares(
        address _user
    )
        external
        view
        returns (uint256);

    function poolToken()
        external
        view
        returns (address);

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

interface IUniswapV2 {

    function swapExactETHForTokens(
        uint256 _amountOutMin,
        address[] calldata _path,
        address _to,
        uint256 _deadline
    )
        external
        payable
        returns (uint256[] memory amounts);

    function swapExactTokensForETH(
        uint256 _amountIn,
        uint256 _amountOutMin,
        address[] calldata _path,
        address _to,
        uint256 _deadline
    )
        external
        returns (uint256[] memory amounts);
}

interface IWETH is IERC20 {

    function deposit()
        payable
        external;

    function withdraw(
        uint256 _amount
    )
        external;
}

interface IWiseLending {

    function depositExactAmount(
        uint256 _nftId,
        address _underlyingToken,
        uint256 _depositAmount
    )
        external
        payable
        returns (uint256);

    function withdrawExactAmount(
        uint256 _nftId,
        address _underlyingToken,
        uint256 _withdrawAmount
    )
        external
        returns (uint256);

    function POSITION_NFT()
        external
        view
        returns (address);

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

interface IWiseSecurity {

    function getPositionLendingAmount(
        uint256 _nftId,
        address _poolToken
    )
        external
        view
        returns (uint256);

    function maximumWithdrawToken(
        uint256 _nftId,
        address _poolToken,
        uint256 _interval,
        uint256 _solelyWithdrawAmount
    )
        external
        view
        returns (uint256);
}

interface IPositionNFTs {

    function mintPosition()
        external
        returns (uint256);

    function mintPositionForUser(
        address _user
    )
        external
        returns (uint256);

    function approve(
        address _to,
        uint256 _tokenId
    )
        external;

    function ownerOf(
        uint256 _nftId
    )
        external
        returns (address);

    function transferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    )
        external;
}
// File: contracts/IERC20.sol



// SPDX-License-Identifier: --DAO--

/**
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

pragma solidity =0.8.21;

interface IERC20 {

    function transfer(
        address _to,
        uint256 _amount
    )
        external;

    function transferFrom(
        address _from,
        address _to,
        uint256 _amount
    )
        external;

    function balanceOf(
        address _account
    )
        external
        view
        returns (uint256);

    function approve(
        address _spender,
        uint256 _amount
    )
        external;

    function allowance(
        address _user,
        address _spender
    )
        external
        view
        returns (uint256);

    function decimals()
        external
        view
        returns (uint8);

    function symbol()
        external
        view
        returns (string memory);
}