contract TranchedPool is BaseUpgradeablePausable, ITranchedPool, SafeERC20Transfer {
  GoldfinchConfig public config;
  using ConfigHelper for GoldfinchConfig;
  using FixedPoint for FixedPoint.Unsigned;
  using FixedPoint for uint256;

  bytes32 public constant LOCKER_ROLE = keccak256("LOCKER_ROLE");
  uint256 public constant FP_SCALING_FACTOR = 1e18;
  uint256 public constant SECONDS_PER_DAY = 60 * 60 * 24;
  uint256 public constant ONE_HUNDRED = 100; // Need this because we cannot call .div on a literal 100
  uint256 public juniorFeePercent;
  bool public drawdownsPaused;

  TrancheInfo internal seniorTranche;
  TrancheInfo internal juniorTranche;

  event DepositMade(address indexed owner, uint256 indexed tranche, uint256 indexed tokenId, uint256 amount);
  event WithdrawalMade(
    address indexed owner,
    uint256 indexed tranche,
    uint256 indexed tokenId,
    uint256 interestWithdrawn,
    uint256 principalWithdrawn
  );

  event PaymentApplied(
    address indexed payer,
    address indexed pool,
    uint256 interestAmount,
    uint256 principalAmount,
    uint256 remainingAmount,
    uint256 reserveAmount
  );
  event SharePriceUpdated(
    address indexed pool,
    uint256 indexed tranche,
    uint256 principalSharePrice,
    int256 principalDelta,
    uint256 interestSharePrice,
    int256 interestDelta
  );
  event ReserveFundsCollected(address indexed from, uint256 amount);
  event CreditLineMigrated(address indexed oldCreditLine, address indexed newCreditLine);
  event DrawdownMade(address indexed borrower, uint256 amount);
  event DrawdownsPaused(address indexed pool);
  event DrawdownsUnpaused(address indexed pool);
  event EmergencyShutdown(address indexed pool);

  function initialize(
    address _config,
    address _borrower,
    uint256 _juniorFeePercent,
    uint256 _limit,
    uint256 _interestApr,
    uint256 _paymentPeriodInDays,
    uint256 _termInDays,
    uint256 _lateFeeApr
  ) public override initializer {
    require(
      address(_config) != address(0) && address(_borrower) != address(0),
      "Config and borrower addresses cannot be empty"
    );

    config = GoldfinchConfig(_config);
    address owner = config.protocolAdminAddress();
    require(owner != address(0), "Owner address cannot be empty");
    __BaseUpgradeablePausable__init(owner);
    seniorTranche = TrancheInfo({
      id: uint256(ITranchedPool.Tranches.Senior),
      principalSharePrice: usdcToSharePrice(1, 1),
      interestSharePrice: 0,
      principalDeposited: 0,
      lockedUntil: 0
    });
    juniorTranche = TrancheInfo({
      id: uint256(ITranchedPool.Tranches.Junior),
      principalSharePrice: usdcToSharePrice(1, 1),
      interestSharePrice: 0,
      principalDeposited: 0,
      lockedUntil: 0
    });
    createAndSetCreditLine(_borrower, _limit, _interestApr, _paymentPeriodInDays, _termInDays, _lateFeeApr);

    createdAt = block.timestamp;
    juniorFeePercent = _juniorFeePercent;

    _setupRole(LOCKER_ROLE, _borrower);
    _setupRole(LOCKER_ROLE, owner);
    _setRoleAdmin(LOCKER_ROLE, OWNER_ROLE);

    // Unlock self for infinite amount
    bool success = config.getUSDC().approve(address(this), uint256(-1));
    require(success, "Failed to approve USDC");
  }

  /**
   * @notice Deposit a USDC amount into the pool for a tranche. Mints an NFT to the caller representing the position
   * @param tranche The number representing the tranche to deposit into
   * @param amount The USDC amount to tranfer from the caller to the pool
   * @return tokenId The tokenId of the NFT
   */
  function deposit(uint256 tranche, uint256 amount)
    public
    override
    nonReentrant
    whenNotPaused
    returns (uint256 tokenId)
  {
    TrancheInfo storage trancheInfo = getTrancheInfo(tranche);
    require(trancheInfo.lockedUntil == 0, "Tranche has been locked");

    trancheInfo.principalDeposited = trancheInfo.principalDeposited.add(amount);
    IPoolTokens.MintParams memory params = IPoolTokens.MintParams({tranche: tranche, principalAmount: amount});
    tokenId = config.getPoolTokens().mint(params, msg.sender);
    safeERC20TransferFrom(config.getUSDC(), msg.sender, address(this), amount);
    emit DepositMade(msg.sender, tranche, tokenId, amount);
    return tokenId;
  }

  function depositWithPermit(
    uint256 tranche,
    uint256 amount,
    uint256 deadline,
    uint8 v,
    bytes32 r,
    bytes32 s
  ) public override returns (uint256 tokenId) {
    IERC20Permit(config.usdcAddress()).permit(msg.sender, address(this), amount, deadline, v, r, s);
    return deposit(tranche, amount);
  }

  /**
   * @notice Withdraw an already deposited amount if the funds are available
   * @param tokenId The NFT representing the position
   * @param amount The amount to withdraw (must be <= interest+principal currently available to withdraw)
   * @return interestWithdrawn The interest amount that was withdrawn
   * @return principalWithdrawn The principal amount that was withdrawn
   */
  function withdraw(uint256 tokenId, uint256 amount)
    public
    override
    onlyTokenHolder(tokenId)
    nonReentrant
    whenNotPaused
    returns (uint256 interestWithdrawn, uint256 principalWithdrawn)
  {
    IPoolTokens.TokenInfo memory tokenInfo = config.getPoolTokens().getTokenInfo(tokenId);
    TrancheInfo storage trancheInfo = getTrancheInfo(tokenInfo.tranche);

    return _withdraw(trancheInfo, tokenInfo, tokenId, amount);
  }

  /**
   * @notice Withdraw from many tokens (that the sender owns) in a single transaction
   * @param tokenIds An array of tokens ids representing the position
   * @param amounts An array of amounts to withdraw from the corresponding tokenIds
   */
  function withdrawMultiple(uint256[] calldata tokenIds, uint256[] calldata amounts) public override {
    require(tokenIds.length == amounts.length, "TokensIds and Amounts must be the same length");

    for (uint256 i = 0; i < amounts.length; i++) {
      withdraw(tokenIds[i], amounts[i]);
    }
  }

  /**
   * @notice Similar to withdraw but will withdraw all available funds
   * @param tokenId The NFT representing the position
   * @return interestWithdrawn The interest amount that was withdrawn
   * @return principalWithdrawn The principal amount that was withdrawn
   */
  function withdrawMax(uint256 tokenId)
    external
    override
    onlyTokenHolder(tokenId)
    nonReentrant
    whenNotPaused
    returns (uint256 interestWithdrawn, uint256 principalWithdrawn)
  {
    IPoolTokens.TokenInfo memory tokenInfo = config.getPoolTokens().getTokenInfo(tokenId);
    TrancheInfo storage trancheInfo = getTrancheInfo(tokenInfo.tranche);

    (uint256 interestRedeemable, uint256 principalRedeemable) = redeemableInterestAndPrincipal(trancheInfo, tokenInfo);

    uint256 amount = interestRedeemable.add(principalRedeemable);

    return _withdraw(trancheInfo, tokenInfo, tokenId, amount);
  }

  /**
   * @notice Draws down the funds (and locks the pool) to the borrower address. Can only be called by the borrower
   * @param amount The amount to drawdown from the creditline (must be < limit)
   */
  function drawdown(uint256 amount) external override onlyLocker whenNotPaused {
    require(!drawdownsPaused, "Drawdowns are currently paused");
    if (!locked()) {
      // Assumes the senior fund has invested already (saves the borrower a separate transaction to lock the pool)
      _lockPool();
    }

    creditLine.drawdown(amount);

    // Update the share price to reflect the amount remaining in the pool
    uint256 amountRemaining = totalDeposited().sub(creditLine.balance());
    uint256 oldJuniorPrincipalSharePrice = juniorTranche.principalSharePrice;
    uint256 oldSeniorPrincipalSharePrice = seniorTranche.principalSharePrice;
    juniorTranche.principalSharePrice = calculateExpectedSharePrice(amountRemaining, juniorTranche);
    seniorTranche.principalSharePrice = calculateExpectedSharePrice(amountRemaining, seniorTranche);

    address borrower = creditLine.borrower();
    safeERC20TransferFrom(config.getUSDC(), address(this), borrower, amount);
    emit DrawdownMade(borrower, amount);
    emit SharePriceUpdated(
      address(this),
      juniorTranche.id,
      juniorTranche.principalSharePrice,
      int256(oldJuniorPrincipalSharePrice.sub(juniorTranche.principalSharePrice)) * -1,
      juniorTranche.interestSharePrice,
      0
    );
    emit SharePriceUpdated(
      address(this),
      seniorTranche.id,
      seniorTranche.principalSharePrice,
      int256(oldSeniorPrincipalSharePrice.sub(seniorTranche.principalSharePrice)) * -1,
      seniorTranche.interestSharePrice,
      0
    );
  }

  /**
   * @notice Locks the junior tranche, preventing more junior deposits. Gives time for the senior to determine how
   * much to invest (ensure leverage ratio cannot change for the period)
   */
  function lockJuniorCapital() external override onlyLocker whenNotPaused {
    _lockJuniorCapital();
  }

  /**
   * @notice Locks the pool (locks both senior and junior tranches and starts the drawdown period). Beyond the drawdown
   * period, any unused capital is available to withdraw by all depositors
   */
  function lockPool() external override onlyLocker whenNotPaused {
    _lockPool();
  }

  /**
   * @notice Triggers an assessment of the creditline and the applies the payments according the tranche waterfall
   */
  function assess() external override whenNotPaused {
    _assess();
  }

  /**
   * @notice Allows repaying the creditline. Collects the USDC amount from the sender and triggers an assess
   * @param amount The amount to repay
   */
  function pay(uint256 amount) external override whenNotPaused {
    require(amount > 0, "Must pay more than zero");

    collectPayment(amount);
    _assess();
  }

  /**
   * @notice Migrates to a new goldfinch config address
   */
  function updateGoldfinchConfig() external onlyAdmin {
    config = GoldfinchConfig(config.configAddress());
    creditLine.updateGoldfinchConfig();
  }

  /**
   * @notice Pauses the pool and sweeps any remaining funds to the treasury reserve.
   */
  function emergencyShutdown() public onlyAdmin {
    if (!paused()) {
      pause();
    }

    IERC20withDec usdc = config.getUSDC();
    address reserveAddress = config.reserveAddress();
    // Sweep any funds to community reserve
    uint256 poolBalance = usdc.balanceOf(address(this));
    if (poolBalance > 0) {
      safeERC20Transfer(usdc, reserveAddress, poolBalance);
    }

    uint256 clBalance = usdc.balanceOf(address(creditLine));
    if (clBalance > 0) {
      safeERC20TransferFrom(usdc, address(creditLine), reserveAddress, clBalance);
    }
    emit EmergencyShutdown(address(this));
  }

  /**
   * @notice Pauses all drawdowns (but not deposits/withdraws)
   */
  function pauseDrawdowns() public onlyAdmin {
    drawdownsPaused = true;
    emit DrawdownsPaused(address(this));
  }

  /**
   * @notice Unpause drawdowns
   */
  function unpauseDrawdowns() public onlyAdmin {
    drawdownsPaused = false;
    emit DrawdownsUnpaused(address(this));
  }

  /**
   * @notice Migrates the accounting variables from the current creditline to a brand new one
   * @param _borrower The borrower address
   * @param _limit The new limit
   * @param _interestApr The new interest APR
   * @param _paymentPeriodInDays The new payment period in days
   * @param _termInDays The new term in days
   * @param _lateFeeApr The new late fee APR
   */
  function migrateCreditLine(
    address _borrower,
    uint256 _limit,
    uint256 _interestApr,
    uint256 _paymentPeriodInDays,
    uint256 _termInDays,
    uint256 _lateFeeApr
  ) public onlyAdmin {
    require(_borrower != address(0), "Borrower must not be empty");
    require(_paymentPeriodInDays != 0, "Payment period must not be empty");
    require(_termInDays != 0, "Term must not be empty");

    address originalClAddr = address(creditLine);
    IV2CreditLine originalCl = IV2CreditLine(originalClAddr);

    createAndSetCreditLine(_borrower, _limit, _interestApr, _paymentPeriodInDays, _termInDays, _lateFeeApr);

    IV2CreditLine newCl = creditLine;
    address newClAddr = address(newCl);

    emit CreditLineMigrated(originalClAddr, newClAddr);

    // Copy over all accounting variables
    newCl.setBalance(originalCl.balance());
    newCl.setInterestOwed(originalCl.interestOwed());
    newCl.setPrincipalOwed(originalCl.principalOwed());
    newCl.setTermEndTime(originalCl.termEndTime());
    newCl.setNextDueTime(originalCl.nextDueTime());
    newCl.setInterestAccruedAsOf(originalCl.interestAccruedAsOf());
    newCl.setLastFullPaymentTime(originalCl.lastFullPaymentTime());
    newCl.setTotalInterestAccrued(originalCl.totalInterestAccrued());

    // Transfer any funds to new CL
    uint256 clBalance = config.getUSDC().balanceOf(originalClAddr);
    if (clBalance > 0) {
      safeERC20TransferFrom(config.getUSDC(), originalClAddr, newClAddr, clBalance);
    }

    // Close out old CL
    originalCl.setBalance(0);
    originalCl.setLimit(0);
  }

  /**
   * @notice Migrates to a new creditline without copying the accounting variables
   */
  function migrateAndSetNewCreditLine(address newCl) public onlyAdmin {
    require(newCl != address(0), "Creditline cannot be empty");
    address originalClAddr = address(creditLine);
    // Transfer any funds to new CL
    uint256 clBalance = config.getUSDC().balanceOf(originalClAddr);
    if (clBalance > 0) {
      safeERC20TransferFrom(config.getUSDC(), originalClAddr, newCl, clBalance);
    }

    // Close out old CL
    creditLine.setBalance(0);
    creditLine.setLimit(0);

    // set new CL
    creditLine = IV2CreditLine(newCl);
    // sanity check that the new address is in fact a creditline
    creditLine.limit();

    emit CreditLineMigrated(originalClAddr, address(creditLine));
  }

  // CreditLine proxy methods, for convenience
  function limit() public view returns (uint256) {
    return creditLine.limit();
  }

  function borrower() public view returns (address) {
    return creditLine.borrower();
  }

  function interestApr() public view returns (uint256) {
    return creditLine.interestApr();
  }

  function paymentPeriodInDays() public view returns (uint256) {
    return creditLine.paymentPeriodInDays();
  }

  function termInDays() public view returns (uint256) {
    return creditLine.termInDays();
  }

  function lateFeeApr() public view returns (uint256) {
    return creditLine.lateFeeApr();
  }

  function getTranche(uint256 tranche) public view override returns (TrancheInfo memory) {
    return getTrancheInfo(tranche);
  }

  /**
   * @notice Converts USDC amounts to share price
   * @param amount The USDC amount to convert
   * @param totalShares The total shares outstanding
   * @return The share price of the input amount
   */
  function usdcToSharePrice(uint256 amount, uint256 totalShares) public pure returns (uint256) {
    return totalShares == 0 ? 0 : amount.mul(FP_SCALING_FACTOR).div(totalShares);
  }

  /**
   * @notice Converts share price to USDC amounts
   * @param sharePrice The share price to convert
   * @param totalShares The total shares outstanding
   * @return The USDC amount of the input share price
   */
  function sharePriceToUsdc(uint256 sharePrice, uint256 totalShares) public pure returns (uint256) {
    return sharePrice.mul(totalShares).div(FP_SCALING_FACTOR);
  }

  /**
   * @notice Determines the amount of interest and principal redeemable by a particular tokenId
   * @param tokenId The token representing the position
   * @return interestRedeemable The interest available to redeem
   * @return principalRedeemable The principal available to redeem
   */
  function availableToWithdraw(uint256 tokenId)
    public
    view
    override
    returns (uint256 interestRedeemable, uint256 principalRedeemable)
  {
    IPoolTokens.TokenInfo memory tokenInfo = config.getPoolTokens().getTokenInfo(tokenId);
    TrancheInfo storage trancheInfo = getTrancheInfo(tokenInfo.tranche);

    if (currentTime() > trancheInfo.lockedUntil) {
      return redeemableInterestAndPrincipal(trancheInfo, tokenInfo);
    } else {
      return (0, 0);
    }
  }

  /* Internal functions  */

  function _withdraw(
    TrancheInfo storage trancheInfo,
    IPoolTokens.TokenInfo memory tokenInfo,
    uint256 tokenId,
    uint256 amount
  ) internal returns (uint256 interestWithdrawn, uint256 principalWithdrawn) {
    (uint256 interestRedeemable, uint256 principalRedeemable) = redeemableInterestAndPrincipal(trancheInfo, tokenInfo);
    uint256 netRedeemable = interestRedeemable.add(principalRedeemable);

    require(amount <= netRedeemable, "Invalid redeem amount");
    require(currentTime() > trancheInfo.lockedUntil, "Tranche is locked");

    // If the tranche has not been locked, ensure the deposited amount is correct
    if (trancheInfo.lockedUntil == 0) {
      trancheInfo.principalDeposited = trancheInfo.principalDeposited.sub(amount);
    }

    uint256 interestToRedeem = Math.min(interestRedeemable, amount);
    uint256 principalToRedeem = Math.min(principalRedeemable, amount.sub(interestToRedeem));

    config.getPoolTokens().redeem(tokenId, principalToRedeem, interestToRedeem);
    safeERC20TransferFrom(config.getUSDC(), address(this), msg.sender, principalToRedeem.add(interestToRedeem));

    emit WithdrawalMade(msg.sender, tokenInfo.tranche, tokenId, interestToRedeem, principalToRedeem);

    return (interestToRedeem, principalToRedeem);
  }

  function redeemableInterestAndPrincipal(TrancheInfo storage trancheInfo, IPoolTokens.TokenInfo memory tokenInfo)
    internal
    view
    returns (uint256 interestRedeemable, uint256 principalRedeemable)
  {
    // This supports withdrawing before or after locking because principal share price starts at 1
    // and is set to 0 on lock. Interest share price is always 0 until interest payments come back, when it increases
    uint256 maxPrincipalRedeemable = sharePriceToUsdc(trancheInfo.principalSharePrice, tokenInfo.principalAmount);
    // The principalAmount is used as the totalShares because we want the interestSharePrice to be expressed as a
    // percent of total loan value e.g. if the interest is 10% APR, the interestSharePrice should approach a max of 0.1.
    uint256 maxInterestRedeemable = sharePriceToUsdc(trancheInfo.interestSharePrice, tokenInfo.principalAmount);

    interestRedeemable = maxInterestRedeemable.sub(tokenInfo.interestRedeemed);
    principalRedeemable = maxPrincipalRedeemable.sub(tokenInfo.principalRedeemed);

    return (interestRedeemable, principalRedeemable);
  }

  function _lockJuniorCapital() internal {
    require(!locked(), "Pool already locked");
    require(juniorTranche.lockedUntil == 0, "Junior tranche already locked");

    juniorTranche.lockedUntil = currentTime().add(config.getDrawdownPeriodInSeconds());
  }

  function _lockPool() internal {
    require(juniorTranche.lockedUntil > 0, "Junior tranche must be locked first");

    creditLine.setLimit(Math.min(totalDeposited(), creditLine.limit()));

    // We start the drawdown period, so backers can withdraw unused capital after borrower draws down
    uint256 lockPeriod = config.getDrawdownPeriodInSeconds();
    seniorTranche.lockedUntil = currentTime().add(lockPeriod);
    juniorTranche.lockedUntil = currentTime().add(lockPeriod);
  }

  function collectInterestAndPrincipal(
    address from,
    uint256 interest,
    uint256 principal
  ) internal returns (uint256 totalReserveAmount) {
    safeERC20TransferFrom(config.getUSDC(), from, address(this), principal.add(interest), "Failed to collect payment");

    (uint256 interestAccrued, uint256 principalAccrued) = getTotalInterestAndPrincipal();
    uint256 reserveFeePercent = ONE_HUNDRED.div(config.getReserveDenominator()); // Convert the denonminator to percent

    uint256 interestRemaining = interest;
    uint256 principalRemaining = principal;

    // First determine the expected share price for the senior tranche. This is the gross amount the senior
    // tranche should receive.
    uint256 expectedInterestSharePrice = calculateExpectedSharePrice(interestAccrued, seniorTranche);
    uint256 expectedPrincipalSharePrice = calculateExpectedSharePrice(principalAccrued, seniorTranche);

    // Deduct the junior fee and the protocol reserve
    uint256 desiredNetInterestSharePrice = scaleByFraction(
      expectedInterestSharePrice,
      ONE_HUNDRED.sub(juniorFeePercent.add(reserveFeePercent)),
      ONE_HUNDRED
    );
    // Collect protocol fee interest received (we've subtracted this from the senior portion above)
    uint256 reserveDeduction = scaleByFraction(interestRemaining, reserveFeePercent, ONE_HUNDRED);
    totalReserveAmount = totalReserveAmount.add(reserveDeduction); // protocol fee
    interestRemaining = interestRemaining.sub(reserveDeduction);

    // Apply the interest remaining so we get up to the netInterestSharePrice
    (interestRemaining, principalRemaining) = applyToTrancheBySharePrice(
      interestRemaining,
      principalRemaining,
      desiredNetInterestSharePrice,
      expectedPrincipalSharePrice,
      seniorTranche
    );

    // Then fill up the junior tranche with all the interest remaining, upto the principal share price
    expectedInterestSharePrice = juniorTranche.interestSharePrice.add(
      usdcToSharePrice(interestRemaining, juniorTranche.principalDeposited)
    );
    expectedPrincipalSharePrice = calculateExpectedSharePrice(principalAccrued, juniorTranche);
    (interestRemaining, principalRemaining) = applyToTrancheBySharePrice(
      interestRemaining,
      principalRemaining,
      expectedInterestSharePrice,
      expectedPrincipalSharePrice,
      juniorTranche
    );

    // All remaining interest and principal is applied towards the junior tranche as interest
    interestRemaining = interestRemaining.add(principalRemaining);
    // Since any principal remaining is treated as interest (there is "extra" interest to be distributed)
    // we need to make sure to collect the protocol fee on the additional interest (we only deducted the
    // fee on the original interest portion)
    reserveDeduction = scaleByFraction(principalRemaining, reserveFeePercent, ONE_HUNDRED);
    totalReserveAmount = totalReserveAmount.add(reserveDeduction);
    interestRemaining = interestRemaining.sub(reserveDeduction);
    principalRemaining = 0;

    (interestRemaining, principalRemaining) = applyToTrancheByAmount(
      interestRemaining.add(principalRemaining),
      0,
      interestRemaining.add(principalRemaining),
      0,
      juniorTranche
    );

    sendToReserve(totalReserveAmount);

    return totalReserveAmount;
  }

  function getTotalInterestAndPrincipal() internal view returns (uint256 interestAccrued, uint256 principalAccrued) {
    interestAccrued = creditLine.totalInterestAccrued();
    principalAccrued = creditLine.principalOwed();
    // Add any remaining balance we have to the principal accrued so expected share price will reflect partial
    // drawdowns appropriately. (e.g. if 300K was drawndown from a 1M loan, current and expected share price should
    // be 0.7 and not 0)
    principalAccrued = principalAccrued.add(totalDeposited().sub(creditLine.balance()));
    return (interestAccrued, principalAccrued);
  }

  function calculateExpectedSharePrice(uint256 amount, TrancheInfo memory tranche) internal view returns (uint256) {
    uint256 sharePrice = usdcToSharePrice(amount, tranche.principalDeposited);
    return scaleByPercentOwnership(sharePrice, tranche);
  }

  // If the senior tranche is locked, then the pool is not open to any more deposits (could throw off leverage ratio)
  function locked() internal view returns (bool) {
    return seniorTranche.lockedUntil > 0;
  }

  function createAndSetCreditLine(
    address _borrower,
    uint256 _limit,
    uint256 _interestApr,
    uint256 _paymentPeriodInDays,
    uint256 _termInDays,
    uint256 _lateFeeApr
  ) internal {
    address _creditLine = config.getGoldfinchFactory().createCreditLine();
    creditLine = IV2CreditLine(_creditLine);
    creditLine.initialize(
      address(config),
      address(this), // Set self as the owner
      _borrower,
      _limit,
      _interestApr,
      _paymentPeriodInDays,
      _termInDays,
      _lateFeeApr
    );
  }

  function getTrancheInfo(uint256 tranche) internal view returns (TrancheInfo storage) {
    require(
      tranche == uint256(ITranchedPool.Tranches.Senior) || tranche == uint256(ITranchedPool.Tranches.Junior),
      "Unsupported tranche"
    );
    TrancheInfo storage trancheInfo = tranche == uint256(ITranchedPool.Tranches.Senior) ? seniorTranche : juniorTranche;
    return trancheInfo;
  }

  function scaleByPercentOwnership(uint256 amount, TrancheInfo memory tranche) internal view returns (uint256) {
    uint256 totalDeposited = juniorTranche.principalDeposited.add(seniorTranche.principalDeposited);
    return scaleByFraction(amount, tranche.principalDeposited, totalDeposited);
  }

  function scaleByFraction(
    uint256 amount,
    uint256 fraction,
    uint256 total
  ) internal pure returns (uint256) {
    FixedPoint.Unsigned memory totalAsFixedPoint = FixedPoint.fromUnscaledUint(total);
    FixedPoint.Unsigned memory fractionAsFixedPoint = FixedPoint.fromUnscaledUint(fraction);
    return fractionAsFixedPoint.div(totalAsFixedPoint).mul(amount).div(FP_SCALING_FACTOR).rawValue;
  }

  function totalDeposited() internal view returns (uint256) {
    return juniorTranche.principalDeposited.add(seniorTranche.principalDeposited);
  }

  function currentTime() internal view virtual returns (uint256) {
    return block.timestamp;
  }

  function applyToTrancheBySharePrice(
    uint256 interestRemaining,
    uint256 principalRemaining,
    uint256 desiredInterestSharePrice,
    uint256 desiredPrincipalSharePrice,
    TrancheInfo storage tranche
  ) internal returns (uint256, uint256) {
    uint256 totalShares = tranche.principalDeposited;

    // If the desired share price is lower, then ignore it, and leave it unchanged
    uint256 principalSharePrice = tranche.principalSharePrice;
    if (desiredPrincipalSharePrice < principalSharePrice) {
      desiredPrincipalSharePrice = principalSharePrice;
    }
    uint256 interestSharePrice = tranche.interestSharePrice;
    if (desiredInterestSharePrice < interestSharePrice) {
      desiredInterestSharePrice = interestSharePrice;
    }
    uint256 interestSharePriceDifference = desiredInterestSharePrice.sub(interestSharePrice);
    uint256 desiredInterestAmount = sharePriceToUsdc(interestSharePriceDifference, totalShares);
    uint256 principalSharePriceDifference = desiredPrincipalSharePrice.sub(principalSharePrice);
    uint256 desiredPrincipalAmount = sharePriceToUsdc(principalSharePriceDifference, totalShares);

    (interestRemaining, principalRemaining) = applyToTrancheByAmount(
      interestRemaining,
      principalRemaining,
      desiredInterestAmount,
      desiredPrincipalAmount,
      tranche
    );
    return (interestRemaining, principalRemaining);
  }

  function applyToTrancheByAmount(
    uint256 interestRemaining,
    uint256 principalRemaining,
    uint256 desiredInterestAmount,
    uint256 desiredPrincipalAmount,
    TrancheInfo storage tranche
  ) internal returns (uint256, uint256) {
    uint256 totalShares = tranche.principalDeposited;
    uint256 newSharePrice;

    (interestRemaining, newSharePrice) = applyToSharePrice(
      interestRemaining,
      tranche.interestSharePrice,
      desiredInterestAmount,
      totalShares
    );
    uint256 oldInterestSharePrice = tranche.interestSharePrice;
    tranche.interestSharePrice = newSharePrice;

    (principalRemaining, newSharePrice) = applyToSharePrice(
      principalRemaining,
      tranche.principalSharePrice,
      desiredPrincipalAmount,
      totalShares
    );
    uint256 oldPrincipalSharePrice = tranche.principalSharePrice;
    tranche.principalSharePrice = newSharePrice;
    emit SharePriceUpdated(
      address(this),
      tranche.id,
      tranche.principalSharePrice,
      int256(tranche.principalSharePrice.sub(oldPrincipalSharePrice)),
      tranche.interestSharePrice,
      int256(tranche.interestSharePrice.sub(oldInterestSharePrice))
    );
    return (interestRemaining, principalRemaining);
  }

  function applyToSharePrice(
    uint256 amountRemaining,
    uint256 currentSharePrice,
    uint256 desiredAmount,
    uint256 totalShares
  ) internal pure returns (uint256, uint256) {
    // If no money left to apply, or don't need any changes, return the original amounts
    if (amountRemaining == 0 || desiredAmount == 0) {
      return (amountRemaining, currentSharePrice);
    }
    if (amountRemaining < desiredAmount) {
      // We don't have enough money to adjust share price to the desired level. So just use whatever amount is left
      desiredAmount = amountRemaining;
    }
    uint256 sharePriceDifference = usdcToSharePrice(desiredAmount, totalShares);
    return (amountRemaining.sub(desiredAmount), currentSharePrice.add(sharePriceDifference));
  }

  function sendToReserve(uint256 amount) internal {
    emit ReserveFundsCollected(address(this), amount);
    safeERC20TransferFrom(
      config.getUSDC(),
      address(this),
      config.reserveAddress(),
      amount,
      "Failed to send to reserve"
    );
  }

  function collectPayment(uint256 amount) internal {
    safeERC20TransferFrom(config.getUSDC(), msg.sender, address(creditLine), amount, "Failed to collect payment");
  }

  function _assess() internal {
    (uint256 paymentRemaining, uint256 interestPayment, uint256 principalPayment) = creditLine.assess();
    if (interestPayment > 0 || principalPayment > 0) {
      uint256 reserveAmount = collectInterestAndPrincipal(
        address(creditLine),
        interestPayment,
        principalPayment.add(paymentRemaining)
      );
      emit PaymentApplied(
        creditLine.borrower(),
        address(this),
        interestPayment,
        principalPayment,
        paymentRemaining,
        reserveAmount
      );
    }
  }

  modifier onlyLocker() {
    require(hasRole(LOCKER_ROLE, msg.sender), "Must have locker role to perform this action");
    _;
  }

  modifier onlyTokenHolder(uint256 tokenId) {
    require(
      config.getPoolTokens().isApprovedOrOwner(msg.sender, tokenId),
      "Only the token owner is allowed to call this function"
    );
    _;
  }
}
function assess() external override whenNotPaused {
  _assess();
}

/**
 * @notice Triggers an assessment of the creditline and the applies the payments according the tranche waterfall
 */