@@ -306,6 +306,8 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
306
306
// check to make sure the default grace period has passed.
307
307
BS.CreditRecord memory cr = _getCreditRecord (borrower);
308
308
309
+ if (cr.state == BS.CreditState.Defaulted) revert Errors.defaultHasAlreadyBeenTriggered ();
310
+
309
311
if (block .timestamp > cr.dueDate) {
310
312
cr = _updateDueInfo (borrower, false , false );
311
313
}
@@ -315,12 +317,8 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
315
317
// plus the grace period.
316
318
if (! isDefaultReady (borrower)) revert Errors.defaultTriggeredTooEarly ();
317
319
318
- if (cr.state == BS.CreditState.Defaulted) revert Errors.defaultHasAlreadyBeenTriggered ();
319
-
320
- // Since the fees and interest are computed at the beginning of a cycle, they are included
321
- // in totalDue by default. In a default case, it does not make sense to include them in
322
- // the defaulted amount since the fees have not really happened at the default time.
323
- losses = cr.unbilledPrincipal + (cr.totalDue - cr.feesAndInterestDue);
320
+ // default amount includes all outstanding principals
321
+ losses = cr.unbilledPrincipal + cr.totalDue - cr.feesAndInterestDue;
324
322
325
323
_creditRecordMapping[borrower].state = BS.CreditState.Defaulted;
326
324
@@ -606,15 +604,27 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
606
604
cr = _updateDueInfo (borrower, false , true );
607
605
}
608
606
609
- uint256 payoffAmount = cr.totalDue + cr.unbilledPrincipal;
607
+ // Computes the final payoff amount. Needs to consider the correction associated with
608
+ // all outstanding principals.
609
+ uint256 payoffCorrection = _feeManager.calcCorrection (
610
+ cr.dueDate,
611
+ _creditRecordStaticMapping[borrower].aprInBps,
612
+ cr.unbilledPrincipal + cr.totalDue - cr.feesAndInterestDue
613
+ );
610
614
611
- // The amount to be applied towards principal
612
- uint256 principalPayment = 0 ;
615
+ uint256 payoffAmount = uint256 (
616
+ int256 (int96 (cr.totalDue + cr.unbilledPrincipal)) + int256 (cr.correction)
617
+ ) - payoffCorrection;
618
+
619
+ bool paidOff = false ;
613
620
614
621
// The amount to be collected from the borrower. When _amount is more than what is needed
615
622
// for payoff, only the payoff amount will be transferred
616
623
uint256 amountToCollect;
617
624
625
+ // The amount to be applied towards principal
626
+ uint256 principalPayment = 0 ;
627
+
618
628
if (amount < cr.totalDue) {
619
629
amountToCollect = amount;
620
630
cr.totalDue = uint96 (cr.totalDue - amount);
@@ -625,72 +635,64 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
625
635
principalPayment = amount - cr.feesAndInterestDue;
626
636
cr.feesAndInterestDue = 0 ;
627
637
}
628
- } else {
638
+ if (cr.state == BS.CreditState.Defaulted)
639
+ _recoverDefaultedAmount (borrower, amountToCollect);
640
+ } else if (amount < payoffAmount) {
629
641
amountToCollect = amount;
630
- principalPayment = amount - cr.feesAndInterestDue;
631
642
632
- uint256 principalCap = cr.unbilledPrincipal + cr.totalDue - cr.feesAndInterestDue;
633
- if (principalPayment > principalCap) principalPayment = principalCap;
634
-
635
- cr.unbilledPrincipal = amount - cr.totalDue >= cr.unbilledPrincipal
636
- ? 0
637
- : uint96 (cr.unbilledPrincipal - (amount - cr.totalDue));
643
+ // Apply extra payments towards principal, reduce unbilledPrincipal amount
644
+ cr.unbilledPrincipal -= uint96 (amount - cr.totalDue);
638
645
646
+ principalPayment = amount - cr.feesAndInterestDue;
647
+ if (principalPayment > 0 ) {
648
+ // If there is principal payment, calcuate new correction
649
+ cr.correction -= int96 (
650
+ uint96 (
651
+ _feeManager.calcCorrection (
652
+ cr.dueDate,
653
+ _creditRecordStaticMapping[borrower].aprInBps,
654
+ principalPayment
655
+ )
656
+ )
657
+ );
658
+ }
639
659
cr.feesAndInterestDue = 0 ;
640
660
cr.totalDue = 0 ;
641
661
cr.missedPeriods = 0 ;
642
- if (cr.state == BS.CreditState.Delayed) cr.state = BS.CreditState.GoodStanding;
643
- }
644
-
645
- if (principalPayment > 0 ) {
646
- // If there is principal payment, calcuate new correction
647
- cr.correction -= int96 (
648
- uint96 (
649
- _feeManager.calcCorrection (
650
- cr.dueDate,
651
- _creditRecordStaticMapping[borrower].aprInBps,
652
- principalPayment
653
- )
654
- )
655
- );
656
- }
657
-
658
- // For account in default, record the recovered principal for the pool.
659
- // Note: correction only impacts interest amount, thus no impact on recovered principal
660
- if (cr.state == BS.CreditState.Defaulted) {
661
- _totalPoolValue += principalPayment;
662
- _creditRecordStaticMapping[borrower].defaultAmount -= uint96 (principalPayment);
663
-
664
- distributeIncome (amountToCollect - principalPayment);
665
- }
666
-
667
- // Computes payoff amount, including correction
668
662
669
- // Since only payback generates generative correction, and all other actions (e.g. drawdown)
670
- // will only increase totalDue, unbilledPrincipal, and move correction to the positive
671
- // side, if abs(cr.correction) is larger than the sum of due and principal, the credit line
672
- // would have been paid off at the last payment when the big negative cr.correction was
673
- // generated. This statement is recursively true. Thus the assertion below.
674
- if (cr.correction < 0 ) assert (payoffAmount > uint96 (0 - cr.correction));
675
-
676
- payoffAmount = uint256 (int256 (payoffAmount) + int256 (cr.correction));
663
+ // Moves account to GoodStanding if it was delayed.
664
+ if (cr.state == BS.CreditState.Delayed) cr.state = BS.CreditState.GoodStanding;
677
665
678
- bool paidOff = false ;
679
- if (amount >= payoffAmount) {
666
+ // Recovers funds to the pool if the account is Defaulted.
667
+ // Only moves it to GoodStanding only after payoff, handled in the payoff branch
668
+ if (cr.state == BS.CreditState.Defaulted)
669
+ _recoverDefaultedAmount (borrower, amountToCollect);
670
+ } else {
671
+ // Payoff logic
672
+ paidOff = true ;
673
+ principalPayment = cr.unbilledPrincipal + cr.totalDue - cr.feesAndInterestDue;
680
674
amountToCollect = payoffAmount;
681
675
682
- // Distribut or reverse income to consume outstanding correction.
683
- // Book income with positive correction, i.e., user had drawdown in the past cycle.
684
- // Reverse income with negative correction, i.e., interest for the entire final pay
685
- // period has been booked and distributed, but the user paid off early, thus negative
686
- // correction and income reverse.
687
- if (cr.correction > 0 ) distributeIncome (uint256 (uint96 (cr.correction)));
688
- else if (cr.correction < 0 ) reverseIncome (uint256 (uint96 (0 - cr.correction)));
676
+ if (cr.state == BS.CreditState.Defaulted) {
677
+ _recoverDefaultedAmount (borrower, amountToCollect);
678
+ } else {
679
+ // Distribut or reverse income to consume outstanding correction.
680
+ // Positive correction is generated becasue of a drawdown within this period,
681
+ // it is not booked or distributed yet, needs to be distributed.
682
+ // Negative correction is generated because of a payment including principal
683
+ // within this period, the extra interest paid is not accounted for yet, thus
684
+ // a reversal.
685
+ // Note: For defaulted account, we do not distributed fees and interests
686
+ // until they are paid. It is handled in _recoverDefaultedAmount().
687
+ cr.correction = cr.correction - int96 (int256 (payoffCorrection));
688
+ if (cr.correction > 0 ) distributeIncome (uint256 (uint96 (cr.correction)));
689
+ else if (cr.correction < 0 ) reverseIncome (uint256 (uint96 (0 - cr.correction)));
690
+ }
689
691
690
692
cr.correction = 0 ;
691
693
cr.unbilledPrincipal = 0 ;
692
694
cr.feesAndInterestDue = 0 ;
693
- paidOff = true ;
695
+ cr.totalDue = 0 ;
694
696
695
697
// Closes the credit line if it is in the final period
696
698
if (cr.remainingPeriods == 0 ) {
@@ -712,10 +714,36 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
712
714
msg .sender
713
715
);
714
716
}
715
-
716
717
return (amountToCollect, paidOff);
717
718
}
718
719
720
+ /**
721
+ * @notice Recovers amount when a payment is paid towards a defaulted account.
722
+ * @dev For any payment after a default, it is applied towards principal losses first.
723
+ * Only after the principal is fully recovered, it is applied towards fees & interest.
724
+ */
725
+ function _recoverDefaultedAmount (address borrower , uint256 amountToCollect ) internal {
726
+ uint96 _defaultAmount = _creditRecordStaticMapping[borrower].defaultAmount;
727
+
728
+ if (_defaultAmount > 0 ) {
729
+ uint256 recoveredPrincipal;
730
+ if (_defaultAmount >= amountToCollect) {
731
+ recoveredPrincipal = amountToCollect;
732
+ } else {
733
+ recoveredPrincipal = _defaultAmount;
734
+ distributeIncome (amountToCollect - recoveredPrincipal);
735
+ }
736
+ _totalPoolValue += recoveredPrincipal;
737
+ _defaultAmount -= uint96 (recoveredPrincipal);
738
+ _creditRecordStaticMapping[borrower].defaultAmount = _defaultAmount;
739
+ } else {
740
+ // note The account is moved out of Defaulted state only if the entire due
741
+ // including principals, fees&Interest are paid off. It is possible for
742
+ // the account to owe fees&Interest after _defaultAmount becomes zero.
743
+ distributeIncome (amountToCollect);
744
+ }
745
+ }
746
+
719
747
/// Checks if the given amount is higher than what is allowed by the pool
720
748
function _maxCreditLineCheck (uint256 amount ) internal view {
721
749
if (amount > _poolConfig.maxCreditLine ()) {
@@ -759,8 +787,10 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
759
787
760
788
if (periodsPassed > 0 ) {
761
789
// Distribute income
762
- if (distributeChargesForLastCycle) distributeIncome (newCharges);
763
- else distributeIncome (newCharges - cr.feesAndInterestDue);
790
+ if (cr.state != BS.CreditState.Defaulted) {
791
+ if (distributeChargesForLastCycle) distributeIncome (newCharges);
792
+ else distributeIncome (newCharges - cr.feesAndInterestDue);
793
+ }
764
794
765
795
if (cr.dueDate > 0 )
766
796
cr.dueDate = uint64 (
0 commit comments