Skip to content

Commit 89292df

Browse files
authored
Address issue #249 and #250 - achieve consistent payoff amount (#251)
* Make payoff amount consistent * Fix default logic * Cleanup, add comments
1 parent 7012cd4 commit 89292df

File tree

3 files changed

+175
-106
lines changed

3 files changed

+175
-106
lines changed

contracts/BaseCreditPool.sol

Lines changed: 94 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
306306
// check to make sure the default grace period has passed.
307307
BS.CreditRecord memory cr = _getCreditRecord(borrower);
308308

309+
if (cr.state == BS.CreditState.Defaulted) revert Errors.defaultHasAlreadyBeenTriggered();
310+
309311
if (block.timestamp > cr.dueDate) {
310312
cr = _updateDueInfo(borrower, false, false);
311313
}
@@ -315,12 +317,8 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
315317
// plus the grace period.
316318
if (!isDefaultReady(borrower)) revert Errors.defaultTriggeredTooEarly();
317319

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;
324322

325323
_creditRecordMapping[borrower].state = BS.CreditState.Defaulted;
326324

@@ -606,15 +604,27 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
606604
cr = _updateDueInfo(borrower, false, true);
607605
}
608606

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+
);
610614

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;
613620

614621
// The amount to be collected from the borrower. When _amount is more than what is needed
615622
// for payoff, only the payoff amount will be transferred
616623
uint256 amountToCollect;
617624

625+
// The amount to be applied towards principal
626+
uint256 principalPayment = 0;
627+
618628
if (amount < cr.totalDue) {
619629
amountToCollect = amount;
620630
cr.totalDue = uint96(cr.totalDue - amount);
@@ -625,72 +635,64 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
625635
principalPayment = amount - cr.feesAndInterestDue;
626636
cr.feesAndInterestDue = 0;
627637
}
628-
} else {
638+
if (cr.state == BS.CreditState.Defaulted)
639+
_recoverDefaultedAmount(borrower, amountToCollect);
640+
} else if (amount < payoffAmount) {
629641
amountToCollect = amount;
630-
principalPayment = amount - cr.feesAndInterestDue;
631642

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);
638645

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+
}
639659
cr.feesAndInterestDue = 0;
640660
cr.totalDue = 0;
641661
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
668662

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;
677665

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;
680674
amountToCollect = payoffAmount;
681675

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+
}
689691

690692
cr.correction = 0;
691693
cr.unbilledPrincipal = 0;
692694
cr.feesAndInterestDue = 0;
693-
paidOff = true;
695+
cr.totalDue = 0;
694696

695697
// Closes the credit line if it is in the final period
696698
if (cr.remainingPeriods == 0) {
@@ -712,10 +714,36 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
712714
msg.sender
713715
);
714716
}
715-
716717
return (amountToCollect, paidOff);
717718
}
718719

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+
719747
/// Checks if the given amount is higher than what is allowed by the pool
720748
function _maxCreditLineCheck(uint256 amount) internal view {
721749
if (amount > _poolConfig.maxCreditLine()) {
@@ -759,8 +787,10 @@ contract BaseCreditPool is BasePool, BaseCreditPoolStorage, ICredit {
759787

760788
if (periodsPassed > 0) {
761789
// 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+
}
764794

765795
if (cr.dueDate > 0)
766796
cr.dueDate = uint64(

0 commit comments

Comments
 (0)