// Contract: MetamorphoPYTStrategy
// Target function: function availableLiquidity() external override view returns (uint256)
 reverts.
     *
     * _Available since v3.1._
     */
    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but also transferring `value` wei to `target`.
     *
     * Requirements:
     *
     * - the calling contract must have an ETH balance of at least `value`.
     * - the called Solidity function must be `payable`.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    /**
     * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
     * with `errorMessage` as a fallback revert reason when `target` reverts.
     *
     * _Available since v3.1._
     */
    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    /**
     * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
     * but performing a static call.
     *
     * _Available since v3.3._
     */
    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    /**
     * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
     * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
     *
     * _Available since v4.8._
     */
    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
                // only check isContract if the call was successful and the return data is empty
                // otherwise we already know that it was a contract
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    /**
     * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
     * revert reason or using the provided one.
     *
     * _Available since v4.3._
     */
    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        // Look for revert reason and bubble it up if present
        if (returndata.length > 0) {
            // The easiest way to bubble the revert reason is using memory via assembly
            /// @solidity memory-safe-assembly
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
}

/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20Upgradeable {
    using AddressUpgradeable for address;

    function safeTransfer(
        IERC20Upgradeable token,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(
        IERC20Upgradeable token,
        address from,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(
        IERC20Upgradeable token,
        address spender,
        uint256 value
    ) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    function safeIncreaseAllowance(
        IERC20Upgradeable token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
    }

    function safeDecreaseAllowance(
        IERC20Upgradeable token,
        address spender,
        uint256 value
    ) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
    }

    function safePermit(
        IERC20PermitUpgradeable token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20Upgradeable token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            // Return data is optional
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
}

interface ILendingProtocol {
  function mint() external returns (uint256);
  function redeem(address account) external returns (uint256);
  function nextSupplyRate(uint256 amount) external view returns (uint256);
  function getAPR() external view returns (uint256);
  function getPriceInToken() external view returns (uint256);
  function token() external view returns (address);
  function underlying() external view returns (address);
  function availableLiquidity() external view returns (uint256);
}

interface IIdleCDO {
  function AATranche() external view returns(address);
  function BBTranche() external view returns(address);
  function AAStaking() external view returns(address);
  function BBStaking() external view returns(address);
  function strategy() external view returns(address);
  function strategyToken() external view returns(address);
  function token() external view returns(address);

  function allowAAWithdraw() external view returns(bool);
  function allowBBWithdraw() external view returns(bool);
  function fee() external view returns(uint256);
  function getApr(address _tranche) external view returns(uint256);
  function getContractValue() external view returns(uint256);
  function trancheAPRSplitRatio() external view returns(uint256);
  function minAprSplitAYS() external view returns(uint256);
  function getCurrentAARatio() external view returns(uint256);
  function tranchePrice(address _tranche) external view returns(uint256);
  function virtualPrice(address _tranche) external view returns(uint256);
  function getIncentiveTokens() external view returns(address[] memory);

  function depositAA(uint256) external returns(uint256);
  function depositBB(uint256) external returns(uint256);
  function withdrawAA(uint256) external returns(uint256);
  function withdrawBB(uint256) external returns(uint256);
}

interface IIdleCDOStrategy {
  function getApr() external view returns(uint256);
}

// This contract should be deployed with a minimal proxy factory
contract IdlePYT is ILendingProtocol {
  using SafeERC20Upgradeable for IERC20Upgradeable;

  address public idleToken;
  // protocol token (AA_tranche_token) address
  address public override token;
  // underlying token (token eg DAI) address
  address public override underlying;
  IERC20Upgradeable public tokenContract;
  IERC20Upgradeable public underlyingContract;

  // contract used for minting/burning tranche tokens
  IIdleCDO public idleCDO;
  // Used for calculating the nextRate
  uint256 internal constant AA_RATIO_LIM_UP = 99000;
  uint256 internal constant AA_RATIO_LIM_DOWN = 50000;
  uint256 internal constant FULL_ALLOC = 100000;
  uint256 internal constant ONE_TRANCHE = 1e18;
  bool public isSenior;

  // Errors
  error Initialized();
  error Unauthorized();

  /**
   * @param _token : tranche token address
   * @param _idleToken : idleToken address
   * @param _cdo : IdleCDO contract address for minting tranche tokens
   */
  function initialize(
    address _token, 
    address _idleToken, 
    address _cdo
  ) external {
    if (address(token) != address(0)) {
      revert Initialized();
    }

    idleCDO = IIdleCDO(_cdo);
    token = _token;
    isSenior = idleCDO.AATranche() == _token;
    tokenContract = IERC20Upgradeable(_token);
    underlying = idleCDO.token();
    underlyingContract = IERC20Upgradeable(idleCDO.token());
    idleToken = _idleToken;
    underlyingContract.safeApprove(_cdo, type(uint256).max);
  }

  /**
   * Throws if called by any account other than IdleToken contract.
   */
  function _onlyIdle() internal view {
    if (msg.sender != idleToken) revert Unauthorized();
  }

  /**
   * Calculate next supply rate for Compound, given an `_amount` supplied
   *
   * @notice this is used for off-chain calculations
   * @param _amount : new underlying amount supplied (eg DAI)
   * @return newAAApr : yearly net rate
   */
  function nextSupplyRate(uint256 _amount)
    external view virtual
    returns (uint256 newAAApr) {
      IERC20Upgradeable _token = IERC20Upgradeable(token);
      IIdleCDO _idleCDO = idleCDO;
      uint256 _tvl = _idleCDO.getContractValue();
      // we use tranchePrice instead of virtualPrice for more efficiency as interest accrued wont 
      // affect too much TVL
      uint256 _tvlAA = _token.totalSupply() * _idleCDO.tranchePrice(address(_token)) / ONE_TRANCHE;
      uint256 _newTvlRatio = (_tvlAA + _amount) * FULL_ALLOC / (_tvl + _amount);
      uint256 _newAprRatio  = _calcNewAPRSplit(_newTvlRatio);
      // we need to get the underlying strategy apr here to calculate the new apr for the tranche
      IIdleCDOStrategy innerStrategy = IIdleCDOStrategy(_idleCDO.strategy());
      // TODO in new CDO strategies we should support a getApr(_amount) similar to nextSupplyRate
      // so to calculate also the impact on the underlying lending protocol used by the PYT. This 
      // can be calculated off-chain in the meantime for the optimal rebalance amount
      newAAApr = innerStrategy.getApr() * _newAprRatio / _newTvlRatio;
      newAAApr = newAAApr * (FULL_ALLOC - idleCDO.fee()) / FULL_ALLOC;
  }

  /**
   * @return current price of tranche token
   */
  function getPriceInToken()
    external view
    returns (uint256) {
      return idleCDO.virtualPrice(address(token));
  }

  /**
   * @return _apr current apr
   */
  function getAPR()
    external view
    returns (uint256 _apr) {
      _apr = idleCDO.getApr(address(token));
      _apr = _apr * (FULL_ALLOC - idleCDO.fee()) / FULL_ALLOC;
  }

  /**
   * Gets all underlying tokens in this contract and mints cTokenLike Tokens
   * tokens are then transferred to msg.sender
   * NOTE: underlying tokens needs to be sent here before calling this
   *
   * @return minted : tranche tokens minted
   */
  function mint()
    external virtual
    returns (uint256 minted) {
      _onlyIdle();
      uint256 balance = underlyingContract.balanceOf(address(this));
      if (balance != 0) {
        if (isSenior) {
          idleCDO.depositAA(balance);
        } else {
          idleCDO.depositBB(balance);
        }
        IERC20Upgradeable _token = tokenContract;
        minted = _token.balanceOf(address(this));
        _token.safeTransfer(msg.sender, minted);
      }
  }

  /**
   * Gets all cTokenLike in this contract and redeems underlying tokens.
   * underlying tokens are then transferred to `_account`
   * NOTE: cTokenLike needs to be sent here before calling this
   *
   * @return tokens underlying tokens redeemd
   */
  function redeem(address _account)
    external virtual
    returns (uint256 tokens) {
      _onlyIdle();
      uint256 _bal = tokenContract.balanceOf(address(this));
      if (isSenior) {
        idleCDO.withdrawAA(_bal);
      } else {
        idleCDO.withdrawBB(_bal);
      }
      IERC20Upgradeable _underlying = underlyingContract;
      tokens = _underlying.balanceOf(address(this));
      _underlying.safeTransfer(_account, tokens);
  }

  /**
   * Get the underlying balance available on the lending protocol
   *
   * @return underlying tokens available
   */
  function availableLiquidity() external virtual view returns (uint256) {
    return idleCDO.getContractValue();
  }

  /**
   * Get the new apr split ratio in IdleCDO. Taken from here https://github.com/Idle-Labs/idle-tranches/blob/448c707a690e20bf2ef3e5a233fa97a329b34eb0/contracts/IdleCDO.sol#L454
   *
   * @return _new new apr split ratio for IdleCDO
   */
  function _calcNewAPRSplit(uint256 ratio) internal view returns (uint256 _new){
    uint256 minSplit = idleCDO.minAprSplitAYS();
    uint256 aux;
    if (ratio >= AA_RATIO_LIM_UP) {
      aux = AA_RATIO_LIM_UP;
    } else if (ratio > minSplit) {
      aux = ratio;
    } else {
      aux = minSplit;
    }
    _new = aux * ratio / FULL_ALLOC;
  }
}

interface IMMVault {
  function maxWithdraw(address owner) external view returns (uint256);
}

// This contract should be deployed with a minimal proxy factory
contract MetamorphoPYTStrategy is IdlePYT {