HIGH severity: A single depositor can grief other depositors on the liquidity deposited for the first epoch. This vulnerability allows a single depositor to exploit the withdrawal mechanism before the first epoch begins, enabling them to claim both principal and interest immediately without waiting for epoch finalization. The root cause lies in the `claimWithdrawRequest` function of `IdleCreditVault.sol`,...

Vulnerable code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

import "../../interfaces/IIdleCDOStrategy.sol";
import "../../interfaces/IERC20Detailed.sol";

import '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol';
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

interface IIdleCDOEpochVariant {
  function isEpochRunning() external view returns (bool);
  function epochEndDate() external view returns (uint256);
}

error NotAllowed();

contract IdleCreditVault is
  Initializable,
  OwnableUpgradeable,
  ERC20Upgradeable,
  ReentrancyGuardUpgradeable,
  IIdleCDOStrategy
{
  using SafeERC20Upgradeable for IERC20Detailed;

  /// @notice underlying token address (pool currency for Clearpool)
  address public override token;
  /// @notice decimals of the underlying asset
  uint256 public override tokenDecimals;
  /// @notice one underlying token
  uint256 public override oneToken;
  /// @notice underlying ERC20 token contract (pool currency for Clearpool)
  IERC20Detailed public underlyingToken;
  /// @notice address of the IdleCDO
  address public idleCDO;
  /// @notice one year, used to calculate the APR
  uint256 public constant YEAR = 365 days;
  /// @notice latest saved apr, already scaled to include the buffer period
  uint256 public lastApr;
  /// @notice address of the borrower
  address public borrower;
  /// @notice address of the manager
  address public manager;
  /// @notice user withdraw requests
  mapping (address => uint256) public withdrawsRequests;
  /// @notice user instant withdraw requests
  mapping (address => uint256) public instantWithdrawsRequests;
  /// @notice total withdraw requests
  uint256 public pendingWithdraws;
  /// @notice pending instant withdraw requests
  uint256 public pendingInstantWithdraws;
  /// @notice counter for epoch deposits
  uint256 public totEpochDeposits;
  /// @notice flag to allow transfers
  bool public canTransfer;
  /// @notice last withdraw request epoch for a user
  mapping (address => uint256) public lastWithdrawRequest;
  /// @notice current epoch number
  uint256 public epochNumber;
  /// @notice unscaled apr
  uint256 public unscaledApr;

  /// @custom:oz-upgrades-unsafe-allow constructor
  constructor() {
    token = address(1);
  }

  /// @notice can be only called once
  /// @param _underlyingToken address of the underlying token (pool currency)
  function initialize(
    address _underlyingToken,
    address _owner,
    address _manager,
    address _borrower,
    string memory borrowerName,
    uint256 _apr
  ) public virtual initializer {
    OwnableUpgradeable.__Ownable_init();
    ReentrancyGuardUpgradeable.__ReentrancyGuard_init();
    require(token == address(0), "Token is already initialized");

    //----- // -------//
    token = _underlyingToken;
    underlyingToken = IERC20Detailed(token);
    tokenDecimals = underlyingToken.decimals();
    oneToken = 10**(tokenDecimals);
    borrower = _borrower;
    manager = _manager;
    // on the first setup we set the lastApr equal to the unscaledApr
    lastApr = _apr;
    unscaledApr = _apr;

    // name will be like: Idle Credit Vault Borrower USDC
    // symbol will be like: BorrowerUSDC
    string memory _symbol = IERC20Detailed(token).symbol();
    ERC20Upgradeable.__ERC20_init(
      _concat(_concat(_concat(string("Idle Credit Vault "), borrowerName), " "), _symbol),
      _concat(borrowerName, _symbol)
    );
    //------//-------//

    transferOwnership(_owner);
  }

  /// @notice strategy token decimals
  /// @dev equal to underlying token decimals
  /// @return number of decimals
  function decimals() public view override returns (uint8) {
    return uint8(tokenDecimals);
  }

  /// @notice strategy token address
  function strategyToken() external view override returns (address) {
    return address(this);
  }

  /// @notice return strategy token price which is always 1
  /// @return price in underlyings
  function price() public view virtual override returns (uint256) {
    return oneToken;
  }

  /// @notice current fixed apr for the epoch
  function getApr() external view returns (uint256) {
    return lastApr;
  }

  /// @notice set manager address
  /// @param _manager address of the new manager
  function setManager(address _manager) external onlyOwner {
    manager = _manager;
  }

  /// @notice set both the scaled and unscaled apr
  /// @dev only cdo and manager can set the apr.
  /// @param _unscaledApr unscaled apr
  /// @param _apr scaled apr
  function setAprs(uint256 _unscaledApr, uint256 _apr) external {
    unscaledApr = _unscaledApr;
    // here we also check that msg.sender is allowed
    setApr(_apr);
  }

  /// @notice set the fixed apr
  /// @dev only cdo and manager can set the apr. If manager manually set apr from 
  /// here it will not be scaled to include the buffer period
  function setApr(uint256 _apr) public {
    address _cdo = idleCDO;

    // if cdo is not yet set we skip the check (this can happen only during the setup)
    if (_cdo != address(0)) {
      if (msg.sender != _cdo && msg.sender != manager) {
        revert NotAllowed();
      }
    }
    lastApr = _apr;
  }

  /// @notice request withdraw of underlying token from the vault
  /// @dev we don't burn strategy tokens here, but we increase the withdraw requests
  /// @param _amount number of tokens to withdraw
  /// @param _user address of the user
  /// @param _netInterest net interest gained in the next epoch
  function requestWithdraw(uint256 _amount, address _user, uint256 _netInterest) external {
    _onlyIdleCDO();
    // burn strategy tokens from cdo (we don't burn interest here, only the principal)
    _burn(msg.sender, _amount - _netInterest);
    // mint equal amount of strategy tokens to the user as receipt (interest included), useful in case of default
    _mint(_user, _amount);

    // increase the withdraw requests for the user
    withdrawsRequests[_user] += _amount;
    // increase the total withdraw requests
    pendingWithdraws += _amount;
    // save the epoch of the last withdraw request (buffer + epochDuration is 1 epoch)
    lastWithdrawRequest[_user] = epochNumber;
  }

  /// @notice claim the withdraw request
  /// @dev we burn the strategy tokens and transfer the underlying tokens
  /// @param _user address of the user
  /// @return amount number of tokens claimed
  function claimWithdrawRequest(address _user) external returns (uint256 amount) {
    _onlyIdleCDO();
    // User should wait at least an epoch before claiming the withdraw. Once the epoch is over user can withdraw 
    // at any time even if a new epoch started. 
    // So if epochNumber is the same as the last withdraw request then we revert. Epoch number is increased at stopEpoch
    // NOTE: If a user does not claim a withdraw request and instead requests another withdraw, he will have to wait
    // for another epoch to claim both requests.
    // NOTE 2: if borrower defaults, old withdraw requests can still be claimed
    if (IIdleCDOEpochVariant(idleCDO).epochEndDate() != 0 && (epochNumber <= lastWithdrawRequest[_user])) {
      revert NotAllowed();
    }
    // get amount of underlyings
    amount = withdrawsRequests[_user];
    // burn strategy tokens 1:1 with the amount of underlyings
    _burn(_user, amount);
    withdrawsRequests[_user] = 0;
    lastWithdrawRequest[_user] = 0;
    underlyingToken.safeTransfer(_user, amount);
  }

  /// @notice request instant withdraw of underlying token from the vault
  /// @dev we burn strategy tokens here, and we increase the instant withdraw requests
  /// @param _amount number of tokens to withdraw
  /// @param _user address of the user
  function requestInstantWithdraw(uint256 _amount, address _user) external {
    _onlyIdleCDO();
    // burn strategy tokens from cdo
    _burn(msg.sender, _amount);
  
    // mint equal amount of strategy tokens to the user as receipt, useful in case of default
    _mint(_user, _amount);

    // increase the instant withdraw requests for the user
    instantWithdrawsRequests[_user] += _amount;
    // increase the total instant withdraw requests
    pendingInstantWithdraws += _amount;
  }

  /// @notice claim the instant withdraw request
  /// @dev we transfer the underlying tokens
  /// @param _user address of the user
  function claimInstantWithdrawRequest(address _user) external {
    _onlyIdleCDO();
    uint256 amount = instantWithdrawsRequests[_user];
    // burn strategy tokens from user
    _burn(_user, amount);

    instantWithdrawsRequests[_user] = 0;
    underlyingToken.safeTransfer(_user, amount);
  }

  /// @notice collect the instant withdraw funds
  /// @dev only IdleCDO can call this function
  /// @param _amount number of tokens to collect
  function collectInstantWithdrawFunds(uint256 _amount) external {
    _onlyIdleCDO();
    pendingInstantWithdraws -= _amount;
    underlyingToken.safeTransferFrom(idleCDO, address(this), _amount);
  }

  /// @notice collect the withdraw funds
  /// @dev only IdleCDO can call this function
  /// @param _amount number of tokens to collect
  function collectWithdrawFunds(uint256 _amount) external {
    _onlyIdleCDO();
    pendingWithdraws -= _amount;
    underlyingToken.safeTransferFrom(idleCDO, address(this), _amount);
  }

  /// @notice Send funds to the IdleCDO
  /// @param _amount number of underlyings to transfer
  function sendInterestAndDeposits(uint256 _amount) external {
    _onlyIdleCDO();
    IERC20Detailed(token).safeTransfer(idleCDO, _amount);
  }

  /// @notice Get funds from IdleCDO and mint strategy tokens. Funds are not sent to the borrower here
  /// @param _amount number of underlyings to transfer
  function deposit(uint256 _amount)
    external
    virtual
    override
    returns (uint256) {
    _onlyIdleCDO();
    underlyingToken.safeTransferFrom(msg.sender, address(this), _amount);
    _mint(msg.sender, _amount);

    if (IIdleCDOEpochVariant(idleCDO).isEpochRunning()) {
      // deposit done on stopEpoch (before setting the var to false) so we reset the counter
      totEpochDeposits = 0;
      epochNumber += 1;
    } else {
      // deposit done between epochs so we increase the counter
      totEpochDeposits += _amount;
    }

    return _amount;
  }
  
  /// @inheritdoc ERC20Upgradeable
  /// @dev we don't allow transfers of strategy tokens normally, transfers are allowed only after a default
  function _transfer(address sender, address recipient, uint256 amount) internal virtual override {
    if (msg.sender != idleCDO && !canTransfer) {
      revert NotAllowed();
    }
    super._transfer(sender, recipient, amount);
  }

  /// @notice allow transfers of strategy tokens
  function allowTransfers() external {
    _onlyIdleCDO();
    canTransfer = true;
  }

  /// @notice allow to update whitelisted address
  function setWhitelistedCDO(address _cdo) external onlyOwner {
    require(_cdo != address(0), "IS_0");
    idleCDO = _cdo;
  }

  /// @notice Emergency method to rescue funds
  /// @param _token address of the token to transfer
  /// @param value amount of `_token` to transfer
  /// @param _to receiver address
  function transferToken(address _token, uint256 value, address _to) external onlyOwner {
    IERC20Detailed(_token).safeTransfer(_to, value);
  }

  /// @notice Modifier to make sure that caller os only the idleCDO contract
  function _onlyIdleCDO() internal view {
    if (msg.sender != idleCDO) {
      revert NotAllowed();
    }
  }

  /// @notice concat 2 strings in a single one
  /// @param a first string
  /// @param b second string
  /// @return new string with a and b concatenated
  function _concat(string memory a, string memory b) internal pure returns (string memory) {
    return string(abi.encodePacked(a, b));
  }

  /// @notice Not used as redeems happens only via requestWithdraw and requestInstantWithdraw
  function redeem(uint256 _amount)
    external
    override
    returns (uint256) {}

  /// @notice Not used as redeems happens only via requestWithdraw and requestInstantWithdraw
  function redeemUnderlying(uint256)
    external
    returns (uint256) {}

  /// @notice not used in this strategy
  function pullStkAAVE() 
    external 
    pure 
    override 
    returns (uint256) {}

  /// @notice not used for this strategy
  function getRewardTokens()
    external
    view
    override
    returns (address[] memory) {}

  /// @notice not used for this strategy
  function redeemRewards(bytes calldata)
    external
    override
    returns (uint256[] memory rewards) {}
}

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

import {IdleCDO} from "./IdleCDO.sol";
import {IKeyring} from "./interfaces/keyring/IKeyring.sol";
import {IdleCDOTranche} from "./IdleCDOTranche.sol";
import {IdleCreditVault} from "./strategies/idle/IdleCreditVault.sol";
import {IERC20Detailed} from "./interfaces/IERC20Detailed.sol";
import {IIdleCDOStrategy} from "./interfaces/IIdleCDOStrategy.sol";
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";

error EpochRunning();
error NotAllowed();
error Default();

/// @title IdleCDO variant that supports epochs. 
/// @dev When epoch is running no deposits or withdrawals are allowed. When epoch ends 
/// lenders can request withdrawals, that will be fullfilled by the end of the next epoch.
/// If the apr for the new epoch is lower than the last one, lenders can request 'instant' 
/// withdrawals that will be fullfilled when the epoch starts and after instantWithdrawDelay (3 days).
/// Funds for instant and normal withdrawals are sent to the strategy contract (IdleCreditVault)
contract IdleCDOEpochVariant is IdleCDO {
  using SafeERC20Upgradeable for IERC20Detailed;

  /// @notice flag to check if epoch is running
  bool public isEpochRunning;
  /// @notice flag to allow AA withdraw requests
  bool public allowAAWithdrawRequest;
  /// @notice flag to allow BB withdraw requests
  bool public allowBBWithdrawRequest;
  /// @notice duration of the epoch
  uint256 public epochDuration;
  /// @notice delay to allow instant withdraw requests after next epoch starts
  uint256 public instantWithdrawDelay;
  /// @notice expected interest for the current epoch
  uint256 public expectedEpochInterest;
  /// @notice end date of the current epoch
  uint256 public epochEndDate;
  /// @notice deadline to allow instant withdraw requests
  uint256 public instantWithdrawDeadline;
  /// @notice apr of the last epoch, unscaled
  uint256 public lastEpochApr;
  /// @notice min apr change to trigger instant withdraw
  uint256 public instantWithdrawAprDelta;
  /// @notice fees from pending withdraw request for the curr epoch
  uint256 public pendingWithdrawFees;
  /// @notice net underlyings gained last epoch
  uint256 public lastEpochInterest;
  /// @notice flag to allow instant withdraws
  bool public allowInstantWithdraw;
  /// @notice flag to completely disable instant withdraw
  bool public disableInstantWithdraw;
  /// @notice flag to check if borrower defaulted
  bool public defaulted;
  /// @notice Keyring wallet checker address
  address public keyring;
  /// @notice keyring policyId
  uint256 public keyringPolicyId;
  /// @notice time between 2 epochs, can be set to 0 to start the next epoch without waiting a specified time
  uint256 public bufferPeriod;
  /// @notice flag for enabling anyone to request a withdraw (needed for liquidations)
  bool public keyringAllowWithdraw;

  event AccrueInterest(uint256 interest, uint256 fees);
  event BorrowerDefault(uint256 funds);

  function _additionalInit() internal virtual override {
    // no unlent perc
    unlentPerc = 0;

    // Set the contract with monotranche as default (can still be changed if needed)
    // losses are split according to tvl, senior has no priority
    lossToleranceBps = FULL_ALLOC;
    // all yield to senior
    isAYSActive = false;
    // deposit directly in the strategy
    directDeposit = true;

    // set epoch params
    epochDuration = 30 days;
    bufferPeriod = 5 days;

    // allow requests for withdrawals
    allowAAWithdrawRequest = true;
    allowBBWithdrawRequest = true;

    // default no instant withdraw allowed
    disableInstantWithdraw = true;

    // scale the apr to include the buffer period
    _setScaledApr(IdleCreditVault(strategy).getApr());
  }

  /// @notice Check if msg sender is owner or manager
  function _checkOnlyOwnerOrManager() internal view {
    if (msg.sender != owner() && msg.sender != IdleCreditVault(strategy).manager()) {
      revert NotAllowed();
    }
  }

  ///
  /// Only owner or manager methods 
  ///

  /// @notice update epoch duration
  /// @dev IMPORTANT: bufferPeriod should not be changed once set otherwise interest calculations will be wrong
  /// @param _epochDuration duration in seconds
  /// @param _bufferPeriod time between 2 epochs
  function setEpochParams(uint256 _epochDuration, uint256 _bufferPeriod) public {
    _checkOnlyOwnerOrManager();
    // cannot set epoch params if epoch is running
    // cannot set epochDuration to 0 as it's reserved for closing the pool
    // and cannot set epochDuration if previously was set to 0 as borrower repaid all funds
    if (isEpochRunning || _epochDuration == 0 || epochDuration == 0) {
      revert NotAllowed();
    }
    epochDuration = _epochDuration;
    bufferPeriod = _bufferPeriod;
  }

  /// @notice update instant withdraw params
  /// @param _delay delay in seconds
  /// @param _aprDelta min apr delta to trigger instant withdraw
  /// @param _disable flag to disable instant withdraw
  function setInstantWithdrawParams(uint256 _delay, uint256 _aprDelta, bool _disable) external {
    _checkOnlyOwnerOrManager();
    if (isEpochRunning) {
      revert EpochRunning();
    }
    instantWithdrawDelay = _delay;
    instantWithdrawAprDelta = _aprDelta;
    disableInstantWithdraw = _disable;
  }

  /// @notice update keyring address
  /// @param _keyring address of the keyring contract
  /// @param _keyringPolicyId policyId to check for wallet
  /// @param _keyringAllowWithdraw flag to allow anyone to request a withdraw
  function setKeyringParams(address _keyring, uint256 _keyringPolicyId, bool _keyringAllowWithdraw) external {
    _checkOnlyOwnerOrManager();
    keyring = _keyring;
    keyringPolicyId = _keyringPolicyId;
    keyringAllowWithdraw = _keyringAllowWithdraw;
  }

  /// @notice Start the epoch. No deposits or withdrawals are allowed after this.
  /// @dev We calculate the total funds that the borrower should return at the end of the epoch
  /// ie interests + fees from normal withdraw requests. We send to the borrower underlyings amounts ie interests + 
  /// new deposits - instant withdraw requests if any. If funds are not enough to satisfy all requests
  /// then borrower should return the difference before instantWithdrawDeadline. After epoch start there
  /// should be no underlyings in this contract
  function startEpoch() external {
    _checkOnlyOwnerOrManager();

    // Check that buffer period passed (and epoch is not running as epochEndDate is set)
    // and that the pool is not closed (ie epochDuration == 0)
    uint256 _epochDuration = epochDuration; 
    if (block.timestamp < (epochEndDate + bufferPeriod) || _epochDuration == 0) {
      revert NotAllowed();
    }

    isEpochRunning = true;
    // prevent deposits
    _pause();

    // prevent withdrawals requests
    allowAAWithdrawRequest = false;
    allowBBWithdrawRequest = false;

    IdleCreditVault _strategy = IdleCreditVault(strategy);

    // calculate expected interest 
    // NOTE: all withdrawal requests, burn tranche tokens and decrease getContractValue,
    // this can be done only prior to the start of the epoch so getContractValue() is the total amount net
    // of all withdrawal requests. We add the fee that we should get for normal pending withdraws
    expectedEpochInterest = _calcInterest(getContractValue()) + pendingWithdrawFees;

    // set expected epoch end date
    epochEndDate = block.timestamp + _epochDuration;
    // set instant withdraw deadline
    instantWithdrawDeadline = block.timestamp + instantWithdrawDelay;

    // transfer in this contract funds from interest payment, that were sent to the strategy in stopEpoch
    _strategy.sendInterestAndDeposits(lastEpochInterest + _strategy.totEpochDeposits());

    // we should first check if there are *instant* redeem requests pending 
    // and if yes we should send as much underlyings as possible to the IdleCreditVault contract
    // if there is any surplus then we send those to the borrower
    uint256 pendingInstant = _strategy.pendingInstantWithdraws();
    uint256 totUnderlyings = _contractTokenBalance(token);

    // if there are more requests than the current underlyings we simply send all underlyings
    // to the IdleCreditVault contract
    if (pendingInstant > totUnderlyings) {
      // transfer funds to strategy
      _strategy.collectInstantWithdrawFunds(totUnderlyings);
      return;
    }
    // otherwise we send the amount needed to satisfy the requests to the strategy 
    _strategy.collectInstantWithdrawFunds(pendingInstant);
    // allow instant withdraws right away without waiting for the deadline
    allowInstantWithdraw = true;
    // and transfer the surplus to the borrower
    try this.sendFundsToBorrower(totUnderlyings - pendingInstant) {
      // funds transferred correctly
    } catch {
      _handleBorrowerDefault(totUnderlyings - pendingInstant);
    }
  }

  /// @notice workaround to have safeTransfer to borrower as external and use it in a try/catch block
  /// @param _amount Amount of underlyings to transfer
  function sendFundsToBorrower(uint256 _amount) external {
     if (msg.sender != address(this)) {
      revert NotAllowed();
    }
    IERC20Detailed(token).safeTransfer(IdleCreditVault(strategy).borrower(), _amount);
  }

  /// @notice Stop epoch, accrue interest to the vault and get funds to fullfill normal
  /// (ie non-instant) withdraw requests from the prev epoch.
  /// @param _newApr New apr to set for the next epoch
  /// @param _interest Interest gained in the epoch. This will overwrite the expected interest
  /// must be 0 if there is no need to overwrite the expected interest and if > 0 then it should
  /// be greater than the pending withdraw fees and newApr must be 0. If `_interest` is 1 then
  /// it is interpreted as a special case where we request everything back from the borrower
  /// @dev Only owner or manager can call this function. Borrower MUST approve this contract
  function stopEpoch(uint256 _newApr, uint256 _interest) public {
    _checkOnlyOwnerOrManager();

    IdleCreditVault _strategy = IdleCreditVault(strategy);
    uint256 _pendingWithdraws = _strategy.pendingWithdraws();
    uint256 _pendingWithdrawFees = pendingWithdrawFees;

    if (
      // Check that epoch is running
      !isEpochRunning || 
      // Check that end date is passed
      block.timestamp < epochEndDate || 
      // Check that there are no pending instant withdraws, ie `getInstantWithdrawFunds` was called
      // before closing the epoch
      _strategy.pendingInstantWithdraws() > 0 ||
      // Check that overridden interest, if passed (ie > 1), is greater than pending withdraw fees and the apr is 0 
      // otherwise withdrawal requests may not be fullfilled as they consider also the interest gained in the next epoch 
      (_interest > 1 && (_interest < _pendingWithdrawFees || _newApr != 0))
    ) {
      revert NotAllowed();
    }

    uint256 _expectedInterest = _interest > 1 ? _interest : expectedEpochInterest;
    uint256 _totBorrowed;
    bool _isRequestingAllFunds = _interest == 1;
    // special case where we get everything back from the borrower
    if (_isRequestingAllFunds) {
      // do not consider underlyings already in this contract
      _totBorrowed = getContractValue() - _contractTokenBalance(token);
      _expectedInterest += _totBorrowed;
    }

    // accrue interest to idleCDO, this will increase tranche prices.
    // Send also tot withdraw requests amount to the IdleCreditVault contract
    try this.getFundsFromBorrower(_expectedInterest, _pendingWithdraws, 0) {
      // transfer in strategy and decrease pendingWithdraws
      if (_pendingWithdraws > 0) {
        _strategy.collectWithdrawFunds(_pendingWithdraws);
      }

      // Transfer pending withdraw fees to feeReceiver before update accounting
      // NOTE: Fees are sent with 2 different transfer calls, here and after updateAccounting, to avoid complicated calculations
      if (_pendingWithdrawFees > 0) {
        IERC20Detailed(token).safeTransfer(feeReceiver, _pendingWithdrawFees);
      }

      if (_isRequestingAllFunds) {
        // we already have strategyTokens equal to _totBorrowed in this contract
        // so we simply transfer _totBorrowed to the strategy to avoid double counting
        // for getContractValue
        IERC20Detailed(token).safeTransfer(address(_strategy), _totBorrowed);
      }

      // update tranche prices and unclaimed fees
      _updateAccounting();

      // transfer fees
      uint256 _fees = unclaimedFees;
      IERC20Detailed(token).safeTransfer(feeReceiver, _fees);
      unclaimedFees = 0;

      // save net gain (this does not include interest gained for pending withdrawals)
      uint256 netInterest = (_isRequestingAllFunds ? _expectedInterest - _totBorrowed : _expectedInterest) - _fees - _pendingWithdrawFees;
      lastEpochInterest = netInterest;
      // mint strategyTokens equal to interest and send underlying to strategy to avoid double counting for NAV
      _strategy.deposit(netInterest);

      // save last apr, unscaled
      lastEpochApr = _strategy.unscaledApr();
      // set apr for next epoch
      _setScaledApr(_newApr);

      // stop epoch
      isEpochRunning = false;
      expectedEpochInterest = 0;
      pendingWithdrawFees = 0;

      // allow deposits
      _unpause();
      // allow withdrawals requests
      allowAAWithdrawRequest = true;
      allowBBWithdrawRequest = true;
      // block instant withdraws claims as these can be done only after the deadline
      // or only if borrower is repaying all funds
      allowInstantWithdraw = _isRequestingAllFunds;

      if (_isRequestingAllFunds) {
        // user will request only normal withdraw and can claim right after
        disableInstantWithdraw = true;
        epochDuration = 0;
        epochEndDate = 0;
      }

      emit AccrueInterest(_expectedInterest - _totBorrowed, _fees + _pendingWithdrawFees);
    } catch {
      isEpochRunning = false;
      // if borrower defaults, prev instant withdraw requests can still be withdrawn
      // as were already fullfilled prior to the default (all funds already sent to the strategy)
      allowInstantWithdraw = true;
      _handleBorrowerDefault(_expectedInterest + _pendingWithdraws);
    }
  }

  /// @notice Stop epoch and set new duration
  /// @dev see stopEpoch and setEpochParams for more details, bufferPeriod is not modified
  /// @param _newApr New apr to set for the next epoch
  /// @param _interest Interest gained in the epoch
  /// @param _duration New epoch duration
  function stopEpochWithDuration(uint256 _newApr, uint256 _interest, uint256 _duration) external {
    // stop epoch checks that msg.sender is allowed
    stopEpoch(_newApr, _interest);
    // buffer period is not changed
    setEpochParams(_duration, bufferPeriod);

    // scale the apr with the new durantion and buffer
    _setScaledApr(_newApr);
  }

  /// @notice The apr should be increased by an amount proportional to the buffer period in this 
  /// way during a buffer period lenders will still get interest. Eg if epoch is 30 days and buffer 
  /// is 5 days and the apr lenders should receive is 10% then _newApr should be 10% * 35/30 = 11.67%.
  /// @param _apr Apr to scale
  function _scaleAprWithBuffer(uint256 _apr) internal view returns (uint256) {
    uint256 _duration = epochDuration;
    return _duration == 0 ? _apr : _apr * (_duration + bufferPeriod) / _duration;
  }

  /// @notice Set the scaled apr for the next epoch
  /// @param _newApr New apr to set for the next epoch
  function _setScaledApr(uint256 _newApr) internal {
    IdleCreditVault(strategy).setAprs(_newApr, _scaleAprWithBuffer(_newApr));
  }

  /// @dev Get interest and funds for fullfill withdraw requests (normal and instant) from borrower,
  /// method is external so it can be used in the try/catch blocks
  /// @param _amount Amount of interest to transfer
  /// @param _withdrawRequests Total withdraw requests
  /// @param _instantWithdrawRequests Total instant withdraw requests
  function getFundsFromBorrower(uint256 _amount, uint256 _withdrawRequests, uint256 _instantWithdrawRequests) external {
    if (msg.sender != address(this)) {
      revert NotAllowed();
    }

    uint256 _tot = _amount + _withdrawRequests + _instantWithdrawRequests;
    if (_tot == 0) {
      return;
    }
    IERC20Detailed(token).safeTransferFrom(IdleCreditVault(strategy).borrower(), address(this), _tot);
  }

  /// @notice Get funds from borrower to fullfill instant withdraw requests
  /// Manager should call this method after instantWithdrawDeadline (when epoch is running)
  function getInstantWithdrawFunds() external {
    _checkOnlyOwnerOrManager();

    // Check that epoch is running and that current time is after the deadline
    if (!isEpochRunning || block.timestamp < instantWithdrawDeadline) {
      revert NotAllowed();
    }

    IdleCreditVault _strategy = IdleCreditVault(strategy);
    uint256 _instantWithdraws = _strategy.pendingInstantWithdraws();
    // transfer funds for instant withdraw to this contract
    try this.getFundsFromBorrower(0, 0, _instantWithdraws) {
      // transfer funds to IdleCreditVault and decrease pendingInstantWithdraws
      if (_instantWithdraws > 0) {
        _strategy.collectInstantWithdrawFunds(_instantWithdraws);
      }
      // allow instant withdraws
      allowInstantWithdraw = true;
    } catch {
      _handleBorrowerDefault(_instantWithdraws);
    }
  }

  /// @notice Handle borrower default
  function _handleBorrowerDefault(uint256 funds) internal {
    defaulted = true;

    // deposits should be already prevented
    if (!paused()) {
      _pause();
    }

    // stop the current epoch
    isEpochRunning = false;

    // prevent withdrawals requests
    allowAAWithdrawRequest = false;
    allowBBWithdrawRequest = false;

    // allow strategyTokens transfers 
    IdleCreditVault(strategy).allowTransfers();

    emit BorrowerDefault(funds);
  }

  /// @notice Prevent deposits and redeems for all classes of tranches
  function _emergencyShutdown(bool) internal override {
    // prevent deposits
    if (!paused()) {
      _pause();
    }
    // prevent withdraws requests
    allowAAWithdrawRequest = false;
    allowBBWithdrawRequest = false;
    // Allow deposits/withdraws (once selectively re-enabled, eg for AA holders)
    // without checking for lending protocol default
    skipDefaultCheck = true;
  }

  /// @notice allow deposits and redeems for all classes of tranches
  /// @dev can be called by the owner only
  function restoreOperations() external override {
    _checkOnlyOwner();
    // Check if the pool was defaulted
    if (defaulted) {
      revert NotAllowed();
    }
    // restore deposits
    if (paused()) {
      _unpause();
    }
    // restore withdraws
    allowAAWithdrawRequest = true;
    allowBBWithdrawRequest = true;
    // Allow deposits/withdraws but checks for lending protocol default
    skipDefaultCheck = false;
  }

  /// 
  /// User methods
  ///

  /// @notice Deposit funds in the vault. Overrides the parent method and adds a check for wallet 
  function _deposit(uint256 _amount, address _tranche, address _referral) internal override whenNotPaused returns (uint256) {
    if (!isWalletAllowed(msg.sender)) {
      revert NotAllowed();
    }
    return super._deposit(_amount, _tranche, _referral);
  }

  /// @notice Request a withdraw from the vault
  /// @param _amount Amount of tranche tokens 
  /// @param _tranche Tranche to withdraw from
  /// @return Amount of underlyings requested
  function requestWithdraw(uint256 _amount, address _tranche) external returns (uint256) {
    address aa = AATranche;
    address bb = BBTranche;
    // check if _tranche is valid and if withdraws for that tranche are allowed and if user is allowed
    if (!(_tranche == aa || _tranche == bb) || 
      (!allowAAWithdrawRequest && _tranche == aa) || 
      (!allowBBWithdrawRequest && _tranche == bb) ||
      (!keyringAllowWithdraw && !isWalletAllowed(msg.sender))
    ) {
      revert NotAllowed();
    }
  
    // we trigger an update accounting to check for eventual losses
    _updateAccounting();

    IdleCreditVault creditVault = IdleCreditVault(strategy);
    uint256 _underlyings = _amount * _tranchePrice(_tranche) / ONE_TRANCHE_TOKEN;
    uint256 _userTrancheTokens = IERC20Detailed(_tranche).balanceOf(msg.sender);

    if (!disableInstantWithdraw) {
      // If apr decresed wrt last epoch, request instant withdraw and burn tranche tokens directly
      // we compare unscaled aprs
      if (lastEpochApr > (creditVault.unscaledApr() + instantWithdrawAprDelta)) {
        // Calc max withdrawable if amount passed is 0
        _underlyings = _amount == 0 ? maxWithdrawableInstant(msg.sender, _tranche) : _underlyings;
        // burn strategy tokens from cdo and mint an equal amount to msg.sender as receipt
        creditVault.requestInstantWithdraw(_underlyings, msg.sender);

        // burn tranche tokens and decrease NAV
        if (_amount == 0) {
          _amount = _userTrancheTokens;
        }
        _withdrawOps(_amount, _underlyings, _tranche);
        return _underlyings;
      }
    }

    // recalculate underlyings considering also interest accrued in the epoch as normal withdraws
    // will still accrue interest for the next epoch
    if (_amount == 0) {
      _underlyings = _userTrancheTokens * _tranchePrice(_tranche) / ONE_TRANCHE_TOKEN;
      _amount = _userTrancheTokens;
    }

    uint256 interest = _calcInterestWithdrawRequest(_underlyings) * _trancheAprRatio(_tranche) / FULL_ALLOC;
    uint256 fees = interest * fee / FULL_ALLOC;
    uint256 netInterest = interest - fees;
    // user is requesting principal + interest of next epoch minus fees
    _underlyings += netInterest;
    // add expected fees to pending withdraw fees counter
    pendingWithdrawFees += fees;
    
    // request normal withdraw, we burn strategy tokens without interest for the new epoch and mint and eq amount to msg.sender
    creditVault.requestWithdraw(_underlyings, msg.sender, netInterest);
    // burn tranche tokens and decrease NAV without interest for the next epoch as it was not yet counted in NAV
    _withdrawOps(_amount, _underlyings - netInterest, _tranche);
    return _underlyings;
  }

  /// @notice Get the tranche apr split ratio
  /// @param _tranche address
  /// @return _aprRatio apr split ratio for the tranche
  function _trancheAprRatio(address _tranche) internal view returns (uint256 _aprRatio) {
    _aprRatio = _tranche == AATranche ? trancheAPRSplitRatio : FULL_ALLOC - trancheAPRSplitRatio;
  }

  /// @notice Calculate the interest of an epoch for the given amount
  /// @param _amount Amount of underlyings
  function _calcInterest(uint256 _amount) internal view returns (uint256) {
    return _amount * (IdleCreditVault(strategy).getApr() / 100) * epochDuration / (365 days * ONE_TRANCHE_TOKEN);
  }

  /// @notice Calculate the interest of an epoch for a withdraw request
  /// @dev to avoid having funds not getting interest during buffer period, the apr 
  /// set in the stopEpoch is higher than then intended one so it will cover also the buffer period
  /// eg epoch = 30 days, buffer = 5 days, then if we want to give 10% apr for all the 35 days then
  /// in stop epoch we set the apr to 10% * 35/30 = 11.67%. For this reason people who instead request
  /// a withdraw should not get the additional interest for the buffer period because they can withdraw
  /// a block after the buffer period starts. So we calculate the interest for the 30 days only,
  /// eg. if apr is set to 11.67% and we want to calculate the interest for 30 days at 10% we need to do the 
  /// the opposite -> 11.67% * 30/35 = 10%
  /// @param _amount Amount of underlyings
  function _calcInterestWithdrawRequest(uint256 _amount) internal view returns (uint256) {
    uint256 _duration = epochDuration;
    return _duration == 0 ? 0 : _calcInterest(_amount) * _duration / (_duration + bufferPeriod);
  }

  /// @notice Get the max amount of underlyings that can be withdrawn by user
  /// @param _user User address
  /// @param _tranche Tranche to withdraw from
  function maxWithdrawable(address _user, address _tranche) external view returns (uint256) {
    uint256 currentUnderlyings = IERC20Detailed(_tranche).balanceOf(_user) * _tranchePrice(_tranche) / ONE_TRANCHE_TOKEN;
    // add interest for one epoch
    uint256 interest = _calcInterestWithdrawRequest(currentUnderlyings) * _trancheAprRatio(_tranche) / FULL_ALLOC;
    // sum and remove fees
    return currentUnderlyings + interest - (interest * fee / FULL_ALLOC);
  }

  /// @notice Get the max amount of underlyings that can be withdrawn instantly by user
  /// @param _user User address
  /// @param _tranche Tranche to withdraw from
  function maxWithdrawableInstant(address _user, address _tranche) public view returns (uint256) {
    return IERC20Detailed(_tranche).balanceOf(_user) * _tranchePrice(_tranche) / ONE_TRANCHE_TOKEN;
  }

  /// @notice Burn tranche tokens and update NAV and trancheAPRSplitRatio
  /// @param _amount Amount of tranche tokens
  /// @param _underlyings Amount of underlyings
  /// @param _tranche Tranche to withdraw from
  function _withdrawOps(uint256 _amount, uint256 _underlyings, address _tranche) internal {
    // burn tranche token
    IdleCDOTranche(_tranche).burn(msg.sender, _amount);

    // update NAV with the _amount of underlyings removed
    if (_tranche == AATranche) {
      lastNAVAA -= _underlyings;
    } else {
      lastNAVBB -= _underlyings;
    }

    // update trancheAPRSplitRatio
    _updateSplitRatio(_getAARatio(true));
  }

  /// @notice Claim a withdraw request from the vault. Can be done when at least 1 epoch passed
  /// since last withdraw request
  function claimWithdrawRequest() external {
    // underlyings requested, here we check that user waited at least one epoch and that borrower
    // did not default upon repayment (old requests can still be claimed)
    IdleCreditVault(strategy).claimWithdrawRequest(msg.sender);
  }

  /// @notice Claim an instant withdraw request from the vault. Can be done when epoch is running
  /// as funds will get transferred from borrower when epoch starts
  function claimInstantWithdrawRequest() external {
    // Check that instant withdraws are available
    if (!allowInstantWithdraw) {
      revert NotAllowed();
    }
    IdleCreditVault(strategy).claimInstantWithdrawRequest(msg.sender);
  }

  /// @notice Check if wallet is allowed to interact with the contract
  /// @param _user User address
  /// @return true if wallet is allowed or keyring address is not set
  function isWalletAllowed(address _user) public view returns (bool) {
    address _keyring = keyring;
    return _keyring == address(0) || IKeyring(_keyring).checkCredential(keyringPolicyId, _user);
  }

  /// 
  /// Overridden method not used in this contract (to reduce bytcode size)
  ///

  /// NOTE: normal withdraw are not allowed
  function withdrawAA(uint256) external override returns (uint256) {}
  function withdrawBB(uint256) external override returns (uint256) {}
  function _withdraw(uint256, address) override pure internal returns (uint256) {}
  function setAllowAAWithdraw(bool) external override {}
  function setAllowBBWithdraw(bool) external override {}
  function liquidate(uint256, bool) external override returns (uint256) {}
  function _liquidate(uint256, bool) internal override returns (uint256) {}
  function setRevertIfTooLow(bool) external override {}
  function setLiquidationTolerance(uint256) external override {}

  /// NOTE: strategy price is alway equal to 1 underlying
  function _checkDefault() override internal {}
  function setSkipDefaultCheck(bool) external override {}
  function setMaxDecreaseDefault(uint256) external override {}

  /// NOTE: harvest is not performed to transfer funds to the strategy (startEpoch is used)
  function harvest(
    bool[] calldata,
    bool[] calldata,
    uint256[] calldata,
    uint256[] calldata,
    bytes[] calldata
  ) public override returns (uint256[][] memory) {}

  /// NOTE: there are no rewards to sell nor incentives
  function _sellAllRewards(IIdleCDOStrategy, uint256[] memory, uint256[] memory, bool[] memory, bytes memory)
    internal override returns (uint256[] memory, uint256[] memory, uint256) {}
  function _sellReward(address, bytes memory, uint256, uint256)
    internal override returns (uint256, uint256) {}
  function setReleaseBlocksPeriod(uint256) external override {}
  function _lockedRewards() internal view override returns (uint256) {}

  /// NOTE: stkIDLE gating is not used
  function toggleStkIDLEForTranche(address) external override {}
  function _checkStkIDLEBal(address, uint256) internal view override {}
  function setStkIDLEPerUnderlying(uint256) external override {}

  /// NOTE: fees are not deposited in this contract
  function _depositFees() internal override {}
  function depositBBRef(uint256, address) external override returns (uint256) {}

  /// NOTE: unlent perc should always be 0 and set in additionalInit
  function setUnlentPerc(uint256) external override {}

  /// NOTE: the vault is either a single tranche (ie all interest to senior and set in additionalInit) or AYS is active
  function setTrancheAPRSplitRatio(uint256) external override {}
}

Vulnerability type: CWE-707|CWE-20