contract Hypervisor is IVault, IUniswapV3MintCallback, IUniswapV3SwapCallback, ERC20Permit {
    using SafeERC20 for IERC20;
    using SafeMath for uint256;
    using SignedSafeMath for int256;

    IUniswapV3Pool public pool;
    IERC20 public token0;
    IERC20 public token1;
    uint24 public fee;
    int24 public tickSpacing;

    int24 public baseLower;
    int24 public baseUpper;
    int24 public limitLower;
    int24 public limitUpper;

    address public owner;
    uint256 public deposit0Max;
    uint256 public deposit1Max;
    uint256 public maxTotalSupply;
    mapping(address => bool) public list;
    bool public whitelisted;
    bool public directDeposit;

    uint256 public constant PRECISION = 1e36;

    // @param _pool Uniswap V3 pool for which liquidity is managed
    // @param _owner Owner of the Hypervisor
    constructor(
        address _pool,
        address _owner,
        string memory name,
        string memory symbol
    ) ERC20Permit(name) ERC20(name, symbol) {
        pool = IUniswapV3Pool(_pool);
        token0 = IERC20(pool.token0());
        token1 = IERC20(pool.token1());
        fee = pool.fee();
        tickSpacing = pool.tickSpacing();

        owner = _owner;

        maxTotalSupply = 0; // no cap
        deposit0Max = uint256(-1);
        deposit1Max = uint256(-1);
        whitelisted = false;
    }

    // @dev Should never be called directly by client, `to` can be frontrun 
    // @param deposit0 Amount of token0 transfered from sender to Hypervisor
    // @param deposit1 Amount of token1 transfered from sender to Hypervisor
    // @param to Address to which liquidity tokens are minted
    // @param from Address from which asset tokens are transferred
    // @return shares Quantity of liquidity tokens minted as a result of deposit
    function deposit(
        uint256 deposit0,
        uint256 deposit1,
        address to,
        address from
    ) external override returns (uint256 shares) {
        require(deposit0 > 0 || deposit1 > 0, "deposits must be nonzero");
        require(deposit0 <= deposit0Max && deposit1 <= deposit1Max, "deposits must be less than maximum amounts");
        require(to != address(0) && to != address(this), "to");
        require(!whitelisted || list[msg.sender], "must be on the list");

        // update fees
        (uint128 baseLiquidity, uint128 limitLiquidity) = zeroBurn();

        uint160 sqrtPrice = TickMath.getSqrtRatioAtTick(currentTick());
        uint256 price = FullMath.mulDiv(uint256(sqrtPrice).mul(uint256(sqrtPrice)), PRECISION, 2**(96 * 2));

        (uint256 pool0, uint256 pool1) = getTotalAmounts();

        uint256 deposit0PricedInToken1 = deposit0.mul(price).div(PRECISION);
        shares = deposit1.add(deposit0PricedInToken1);

        if (deposit0 > 0) {
          token0.safeTransferFrom(from, address(this), deposit0);
        }
        if (deposit1 > 0) {
          token1.safeTransferFrom(from, address(this), deposit1);
        }

        if (totalSupply() != 0) {
          uint256 pool0PricedInToken1 = pool0.mul(price).div(PRECISION);
          shares = shares.mul(totalSupply()).div(pool0PricedInToken1.add(pool1));

          if (directDeposit) {
            baseLiquidity = _liquidityForAmounts(
                baseLower,
                baseUpper,
                token0.balanceOf(address(this)),
                token1.balanceOf(address(this))
            );
            _mintLiquidity(baseLower, baseUpper, baseLiquidity, address(this));

            limitLiquidity = _liquidityForAmounts(
                limitLower,
                limitUpper,
                token0.balanceOf(address(this)),
                token1.balanceOf(address(this))
            );
            _mintLiquidity(limitLower, limitUpper, limitLiquidity, address(this));
          }
        }
        _mint(to, shares);
        emit Deposit(from, to, shares, deposit0, deposit1);
        // Check total supply cap not exceeded. A value of 0 means no limit.
        require(maxTotalSupply == 0 || totalSupply() <= maxTotalSupply, "maxTotalSupply");
    }

    function zeroBurn() internal returns(uint128 baseLiquidity, uint128 limitLiquidity) {
      // update fees for inclusion
      (baseLiquidity, , ) = _position(baseLower, baseUpper);
      if (baseLiquidity > 0) {
          pool.burn(baseLower, baseUpper, 0);
      }
      (limitLiquidity, , ) = _position(limitLower, limitUpper);
      if (limitLiquidity > 0) {
          pool.burn(limitLower, limitUpper, 0);
      }
    }

    function pullLiquidity(
      uint256 shares
    ) external onlyOwner returns(
        uint256 base0,
        uint256 base1,
        uint256 limit0,
        uint256 limit1
      ) {
        zeroBurn();
        (base0, base1) = _burnLiquidity(
            baseLower,
            baseUpper,
            _liquidityForShares(baseLower, baseUpper, shares),
            address(this),
            false
        );
        (limit0, limit1) = _burnLiquidity(
            limitLower,
            limitUpper,
            _liquidityForShares(limitLower, limitUpper, shares),
            address(this),
            false
        );
    } 

    // @param shares Number of liquidity tokens to redeem as pool assets
    // @param to Address to which redeemed pool assets are sent
    // @param from Address from which liquidity tokens are sent
    // @return amount0 Amount of token0 redeemed by the submitted liquidity tokens
    // @return amount1 Amount of token1 redeemed by the submitted liquidity tokens
    function withdraw(
        uint256 shares,
        address to,
        address from
    ) external override returns (uint256 amount0, uint256 amount1) {
        require(shares > 0, "shares");
        require(to != address(0), "to");

        // update fees
        zeroBurn();

        // Withdraw liquidity from Uniswap pool
        (uint256 base0, uint256 base1) = _burnLiquidity(
            baseLower,
            baseUpper,
            _liquidityForShares(baseLower, baseUpper, shares),
            to,
            false
        );
        (uint256 limit0, uint256 limit1) = _burnLiquidity(
            limitLower,
            limitUpper,
            _liquidityForShares(limitLower, limitUpper, shares),
            to,
            false
        );

        // Push tokens proportional to unused balances
        uint256 supply = totalSupply();
        uint256 unusedAmount0 = token0.balanceOf(address(this)).mul(shares).div(supply);
        uint256 unusedAmount1 = token1.balanceOf(address(this)).mul(shares).div(supply);
        if (unusedAmount0 > 0) token0.safeTransfer(to, unusedAmount0);
        if (unusedAmount1 > 0) token1.safeTransfer(to, unusedAmount1);

        amount0 = base0.add(limit0).add(unusedAmount0);
        amount1 = base1.add(limit1).add(unusedAmount1);

        require(
            from == msg.sender || IUniversalVault(from).owner() == msg.sender,
            "Sender must own the tokens"
        );
        _burn(from, shares);

        emit Withdraw(from, to, shares, amount0, amount1);
    }

    // @param _baseLower The lower tick of the base position
    // @param _baseUpper The upper tick of the base position
    // @param _limitLower The lower tick of the limit position
    // @param _limitUpper The upper tick of the limit position
    // @param feeRecipient Address of recipient of 10% of earned fees since last rebalance
    // @param swapQuantity Quantity of tokens to swap; if quantity is positive,
    // `swapQuantity` token0 are swaped for token1, if negative, `swapQuantity`
    // token1 is swaped for token0
    function rebalance(
        int24 _baseLower,
        int24 _baseUpper,
        int24 _limitLower,
        int24 _limitUpper,
        address feeRecipient,
        int256 swapQuantity
    ) external override onlyOwner {
        require(
            _baseLower < _baseUpper &&
                _baseLower % tickSpacing == 0 &&
                _baseUpper % tickSpacing == 0,
            "base position invalid"
        );
        require(
            _limitLower < _limitUpper &&
                _limitLower % tickSpacing == 0 &&
                _limitUpper % tickSpacing == 0,
            "limit position invalid"
        );
        require(
          _limitUpper != _baseUpper ||
          _limitLower != _baseLower,
          "limit equals base"
        );

        // update fees
        (uint128 baseLiquidity, uint128 limitLiquidity) = zeroBurn();

        // Withdraw all liquidity and collect all fees from Uniswap pool
        (, uint256 feesLimit0, uint256 feesLimit1) = _position(baseLower, baseUpper);
        (, uint256 feesBase0, uint256 feesBase1) = _position(limitLower, limitUpper);

        uint256 fees0 = feesBase0.add(feesLimit0);
        uint256 fees1 = feesBase1.add(feesLimit1);

        (baseLiquidity, , ) = _position(baseLower, baseUpper);
        (limitLiquidity, , ) = _position(limitLower, limitUpper);

        _burnLiquidity(baseLower, baseUpper, baseLiquidity, address(this), true);
        _burnLiquidity(limitLower, limitUpper, limitLiquidity, address(this), true);

        // transfer 10% of fees for VISR buybacks
        if (fees0 > 0) token0.safeTransfer(feeRecipient, fees0.div(10));
        if (fees1 > 0) token1.safeTransfer(feeRecipient, fees1.div(10));

        emit Rebalance(
            currentTick(),
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this)),
            fees0,
            fees1,
            totalSupply()
        );

        // swap tokens if required
        if (swapQuantity != 0) {
            pool.swap(
                address(this),
                swapQuantity > 0,
                swapQuantity > 0 ? swapQuantity : -swapQuantity,
                swapQuantity > 0 ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1,
                abi.encode(address(this))
            );
        }

        baseLower = _baseLower;
        baseUpper = _baseUpper;
        baseLiquidity = _liquidityForAmounts(
            baseLower,
            baseUpper,
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this))
        );
        _mintLiquidity(baseLower, baseUpper, baseLiquidity, address(this));

        limitLower = _limitLower;
        limitUpper = _limitUpper;
        limitLiquidity = _liquidityForAmounts(
            limitLower,
            limitUpper,
            token0.balanceOf(address(this)),
            token1.balanceOf(address(this))
        );
        _mintLiquidity(limitLower, limitUpper, limitLiquidity, address(this));
    }

    function pendingFees() external onlyOwner returns (uint256 fees0, uint256 fees1) {
        // update fees
        (uint128 baseLiquidity, , ) = _position(baseLower, baseUpper);
        if (baseLiquidity > 0) {
            pool.burn(baseLower, baseUpper, 0);
        }
        (uint128 limitLiquidity, , ) = _position(limitLower, limitUpper);
        if (limitLiquidity > 0) {
            pool.burn(limitLower, limitUpper, 0);
        }

        // Withdraw all liquidity and collect all fees from Uniswap pool
        (, uint256 feesLimit0, uint256 feesLimit1) = _position(baseLower, baseUpper);
        (, uint256 feesBase0, uint256 feesBase1) = _position(limitLower, limitUpper);

        fees0 = feesBase0.add(feesLimit0);
        fees1 = feesBase1.add(feesLimit1);
    }

    function addBaseLiquidity(uint256 amount0, uint256 amount1) external onlyOwner {
        uint128 baseLiquidity = _liquidityForAmounts(
            baseLower,
            baseUpper,
            amount0 == 0 && amount1 == 0 ? token0.balanceOf(address(this)) : amount0,
            amount0 == 0 && amount1 == 0 ? token1.balanceOf(address(this)) : amount1
        );
        _mintLiquidity(baseLower, baseUpper, baseLiquidity, address(this));
    }

    function addLimitLiquidity(uint256 amount0, uint256 amount1) external onlyOwner {
        uint128 limitLiquidity = _liquidityForAmounts(
            limitLower,
            limitUpper,
            amount0 == 0 && amount1 == 0 ? token0.balanceOf(address(this)) : amount0,
            amount0 == 0 && amount1 == 0 ? token1.balanceOf(address(this)) : amount1
        );
        _mintLiquidity(limitLower, limitUpper, limitLiquidity, address(this));
    }

    function _mintLiquidity(
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity,
        address payer
    ) internal returns (uint256 amount0, uint256 amount1) {
        if (liquidity > 0) {
            (amount0, amount1) = pool.mint(
                address(this),
                tickLower,
                tickUpper,
                liquidity,
                abi.encode(payer)
            );
        }
    }

    function _burnLiquidity(
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity,
        address to,
        bool collectAll
    ) internal returns (uint256 amount0, uint256 amount1) {
        if (liquidity > 0) {
            // Burn liquidity
            (uint256 owed0, uint256 owed1) = pool.burn(tickLower, tickUpper, liquidity);

            // Collect amount owed
            uint128 collect0 = collectAll ? type(uint128).max : _uint128Safe(owed0);
            uint128 collect1 = collectAll ? type(uint128).max : _uint128Safe(owed1);
            if (collect0 > 0 || collect1 > 0) {
                (amount0, amount1) = pool.collect(to, tickLower, tickUpper, collect0, collect1);
            }
        }
    }

    function _liquidityForShares(
        int24 tickLower,
        int24 tickUpper,
        uint256 shares
    ) internal view returns (uint128) {
        (uint128 position, , ) = _position(tickLower, tickUpper);
        return _uint128Safe(uint256(position).mul(shares).div(totalSupply()));
    }

    function _position(int24 tickLower, int24 tickUpper)
        internal
        view
        returns (
            uint128 liquidity,
            uint128 tokensOwed0,
            uint128 tokensOwed1
        )
    {
        bytes32 positionKey = keccak256(abi.encodePacked(address(this), tickLower, tickUpper));
        (liquidity, , , tokensOwed0, tokensOwed1) = pool.positions(positionKey);
    }

    function uniswapV3MintCallback(
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external override {
        require(msg.sender == address(pool));
        address payer = abi.decode(data, (address));

        if (payer == address(this)) {
            if (amount0 > 0) token0.safeTransfer(msg.sender, amount0);
            if (amount1 > 0) token1.safeTransfer(msg.sender, amount1);
        } else {
            if (amount0 > 0) token0.safeTransferFrom(payer, msg.sender, amount0);
            if (amount1 > 0) token1.safeTransferFrom(payer, msg.sender, amount1);
        }
    }

    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external override {
        require(msg.sender == address(pool));
        address payer = abi.decode(data, (address));

        if (amount0Delta > 0) {
            if (payer == address(this)) {
                token0.safeTransfer(msg.sender, uint256(amount0Delta));
            } else {
                token0.safeTransferFrom(payer, msg.sender, uint256(amount0Delta));
            }
        } else if (amount1Delta > 0) {
            if (payer == address(this)) {
                token1.safeTransfer(msg.sender, uint256(amount1Delta));
            } else {
                token1.safeTransferFrom(payer, msg.sender, uint256(amount1Delta));
            }
        }
    }

    // @return total0 Quantity of token0 in both positions and unused in the Hypervisor
    // @return total1 Quantity of token1 in both positions and unused in the Hypervisor
    function getTotalAmounts() public view override returns (uint256 total0, uint256 total1) {
        (, uint256 base0, uint256 base1) = getBasePosition();
        (, uint256 limit0, uint256 limit1) = getLimitPosition();
        total0 = token0.balanceOf(address(this)).add(base0).add(limit0);
        total1 = token1.balanceOf(address(this)).add(base1).add(limit1);
    }

    // @return liquidity Amount of total liquidity in the base position
    // @return amount0 Estimated amount of token0 that could be collected by
    // burning the base position
    // @return amount1 Estimated amount of token1 that could be collected by
    // burning the base position
    function getBasePosition()
        public
        view
        returns (
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        )
    {
        (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position(
            baseLower,
            baseUpper
        );
        (amount0, amount1) = _amountsForLiquidity(baseLower, baseUpper, positionLiquidity);
        amount0 = amount0.add(uint256(tokensOwed0));
        amount1 = amount1.add(uint256(tokensOwed1));
        liquidity = positionLiquidity;
    }

    // @return liquidity Amount of total liquidity in the limit position
    // @return amount0 Estimated amount of token0 that could be collected by
    // burning the limit position
    // @return amount1 Estimated amount of token1 that could be collected by
    // burning the limit position
    function getLimitPosition()
        public
        view
        returns (
            uint128 liquidity,
            uint256 amount0,
            uint256 amount1
        )
    {
        (uint128 positionLiquidity, uint128 tokensOwed0, uint128 tokensOwed1) = _position(
            limitLower,
            limitUpper
        );
        (amount0, amount1) = _amountsForLiquidity(limitLower, limitUpper, positionLiquidity);
        amount0 = amount0.add(uint256(tokensOwed0));
        amount1 = amount1.add(uint256(tokensOwed1));
        liquidity = positionLiquidity;
    }

    function _amountsForLiquidity(
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity
    ) internal view returns (uint256, uint256) {
        (uint160 sqrtRatioX96, , , , , , ) = pool.slot0();
        return
            LiquidityAmounts.getAmountsForLiquidity(
                sqrtRatioX96,
                TickMath.getSqrtRatioAtTick(tickLower),
                TickMath.getSqrtRatioAtTick(tickUpper),
                liquidity
            );
    }

    function _liquidityForAmounts(
        int24 tickLower,
        int24 tickUpper,
        uint256 amount0,
        uint256 amount1
    ) internal view returns (uint128) {
        (uint160 sqrtRatioX96, , , , , , ) = pool.slot0();
        return
            LiquidityAmounts.getLiquidityForAmounts(
                sqrtRatioX96,
                TickMath.getSqrtRatioAtTick(tickLower),
                TickMath.getSqrtRatioAtTick(tickUpper),
                amount0,
                amount1
            );
    }

    // @return tick Uniswap pool's current price tick
    function currentTick() public view returns (int24 tick) {
        (, tick, , , , , ) = pool.slot0();
    }

    function _uint128Safe(uint256 x) internal pure returns (uint128) {
        assert(x <= type(uint128).max);
        return uint128(x);
    }

    // @param _maxTotalSupply The maximum liquidity token supply the contract allows
    function setMaxTotalSupply(uint256 _maxTotalSupply) external onlyOwner {
        maxTotalSupply = _maxTotalSupply;
    }

    // @param _deposit0Max The maximum amount of token0 allowed in a deposit
    // @param _deposit1Max The maximum amount of token1 allowed in a deposit
    function setDepositMax(uint256 _deposit0Max, uint256 _deposit1Max) external onlyOwner {
        deposit0Max = _deposit0Max;
        deposit1Max = _deposit1Max;
    }

    function appendList(address[] memory listed) external onlyOwner {
        for (uint8 i; i < listed.length; i++) {
            list[listed[i]] = true;
        }
    }

    function removeListed(address listed) external onlyOwner {
        list[listed] = false;
    }

    function toggleDirectDeposit() external onlyOwner {
        directDeposit = !directDeposit;
    }

    function toggleWhitelist() external onlyOwner {
        whitelisted = !whitelisted;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        owner = newOwner;
    }

    modifier onlyOwner {
        require(msg.sender == owner, "only owner");
        _;
    }
}
// @title Hypervisor
// @notice A Uniswap V2-like interface with fungible liquidity to Uniswap V3
// which allows for arbitrary liquidity provision: one-sided, lop-sided, and
// balanced
function setDepositMax(uint256 _deposit0Max, uint256 _deposit1Max) external onlyOwner {
    deposit0Max = _deposit0Max;
    deposit1Max = _deposit1Max;
}

// @param _deposit0Max The maximum amount of token0 allowed in a deposit
// @param _deposit1Max The maximum amount of token1 allowed in a deposit