Skip to content
This repository has been archived by the owner on Oct 27, 2024. It is now read-only.

KupiaSec - Borrowers can circumvent fees by calling OCC_Modular::callLoan when the grace period exceeds the payment interval #538

Closed
sherlock-admin3 opened this issue Apr 25, 2024 · 1 comment
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue

Comments

@sherlock-admin3
Copy link

sherlock-admin3 commented Apr 25, 2024

KupiaSec

high

Borrowers can circumvent fees by calling OCC_Modular::callLoan when the grace period exceeds the payment interval

Summary

The grace period may be longer than the payment interval, and the borrower may miss serveral loan payments before a loan enters default (You can check it here).
In this scenario, the borrower is required to pay the principal amount, accrued interest, and applicable late fees for any missed payments. However, the borrower may be able to circumvent a portion of the interest and late fees by calling the OCC_Modular::callLoan function, potentially resulting in financial loss to the protocol.

Vulnerability Detail

When borrowers make a payment to the protocol using the makePayment function, they are required to pay the prinicipalOwed, the interestOwed and the lateFee, all of which are calculated at L578. Additionally, the paymentDueBy timestamp is updated by adding the paymentInterval.

zivoe-core-foundry\src\lockers\OCC\OCC_Modular.sol
575: function makePayment(uint256 id) external nonReentrant {
576:         require(loans[id].state == LoanState.Active, "OCC_Modular::makePayment() loans[id].state != LoanState.Active");
577: 
578:         (uint256 principalOwed, uint256 interestOwed, uint256 lateFee,) = amountOwed(id);
579: 
580:         emit PaymentMade(
581:             id, _msgSender(), principalOwed + interestOwed + lateFee, principalOwed,
582:             interestOwed, lateFee, loans[id].paymentDueBy + loans[id].paymentInterval
583:         );
584: 
585:         // Transfer interest + lateFee to YDL if in same format, otherwise keep here for 1INCH forwarding.
586:         if (stablecoin == IZivoeYDL_OCC(IZivoeGlobals_OCC(GBL).YDL()).distributedAsset()) {
587:             IERC20(stablecoin).safeTransferFrom(_msgSender(), IZivoeGlobals_OCC(GBL).YDL(), interestOwed + lateFee);
588:         }
589:         else {
590:             IERC20(stablecoin).safeTransferFrom(_msgSender(), OCT_YDL, interestOwed + lateFee);
591:         }
592:         if (principalOwed > 0) { IERC20(stablecoin).safeTransferFrom(_msgSender(), owner(), principalOwed); }
593: 
594:         if (loans[id].paymentsRemaining == 1) {
595:             loans[id].state = LoanState.Repaid;
596:             loans[id].paymentDueBy = 0;
597:         }
598:         else { loans[id].paymentDueBy += loans[id].paymentInterval; }
599: 
600:         loans[id].principalOwed -= principalOwed;
601:         loans[id].paymentsRemaining -= 1;
602:     }

The prinicipalOwed, interestOwed, and lateFee are calculated within the amountOwed function as follows:

zivoe-core-foundry\src\lockers\OCC\OCC_Modular.sol
440: function amountOwed(uint256 id) public view returns (
441:         uint256 principal, uint256 interest, uint256 lateFee, uint256 total
442:     ) {
443:         // 0 == Bullet.
444:         if (loans[id].paymentSchedule == 0) {
445:             if (loans[id].paymentsRemaining == 1) { principal = loans[id].principalOwed; }
446:         }
447:         // 1 == Amortization (only two options, use else here).
448:         else { principal = loans[id].principalOwed / loans[id].paymentsRemaining; }
449: 
450:         // Add late fee if past loans[id].paymentDueBy.
451:         if (block.timestamp > loans[id].paymentDueBy && loans[id].state == LoanState.Active) {
452:             lateFee = loans[id].principalOwed * (block.timestamp - loans[id].paymentDueBy) * //@audit late fee
453:                 loans[id].APRLateFee / (86400 * 365 * BIPS);
454:         }
455:         interest = loans[id].principalOwed * loans[id].paymentInterval * loans[id].APR / (86400 * 365 * BIPS); //@audit interest of the interval
456:         total = principal + interest + lateFee;
457:     }

As shown in the previous code snippet, the lateFee is calculated based on the difference between the current timestamp and the paymentDueBy timestamp. Additionally, the interest is calculated for the duration of the paymentInterval.

Let's consider the following scenario:

  • Alice has a bullet loan with a grace period of 14 days and a payment interval of 7 days. The loan term is 5 periods.

  • Alice has missed the third paymentDueBy, and the 14-day grace period is nearly expired. At this stage, near the end of the loan term, Alice should be required to call the makePayment function three times, repaying the principal amount, the interest accrued over the three missed payment intervals, and the applicable late fees.

  • In the first makePayment call, Alice would owe 14 days' worth of late fees, the interest for one payment interval, and the paymentDueBy timestamp would be updated by another 7-day interval.

  • For the second makePayment call, the late fees would be 7 days, with interest for another payment interval, and the paymentDueBy would be updated again.

  • On the final call, there would be no late fees, only the principal and interest for the last payment interval.

In total, Alice should be required to pay the principal amount, the interest accrued over the three missed payment intervals, and the late fees accumulated over 21 days.

However, instead of making these three separate makePayment calls, Alice has opted to call the callLoan function.

zivoe-core-foundry\src\lockers\OCC\OCC_Modular.sol
492: function callLoan(uint256 id) external nonReentrant {
493:         require(
494:             _msgSender() == loans[id].borrower || IZivoeGlobals_OCC(GBL).isLocker(_msgSender()), 
495:             "OCC_Modular::callLoan() _msgSender() != loans[id].borrower && !isLocker(_msgSender())"
496:         );
497:         require(loans[id].state == LoanState.Active, "OCC_Modular::callLoan() loans[id].state != LoanState.Active");
498: 
499:         uint256 principalOwed = loans[id].principalOwed;
500:         (, uint256 interestOwed, uint256 lateFee,) = amountOwed(id);
501: 
502:         emit LoanCalled(id, principalOwed + interestOwed + lateFee, principalOwed, interestOwed, lateFee);
503: 
504:         // Transfer interest to YDL if in same format, otherwise keep here for 1INCH forwarding.
505:         if (stablecoin == IZivoeYDL_OCC(IZivoeGlobals_OCC(GBL).YDL()).distributedAsset()) {
506:             IERC20(stablecoin).safeTransferFrom(_msgSender(), IZivoeGlobals_OCC(GBL).YDL(), interestOwed + lateFee);
507:         }
508:         else {
509:             IERC20(stablecoin).safeTransferFrom(_msgSender(), OCT_YDL, interestOwed + lateFee);
510:         }
511: 
512:         IERC20(stablecoin).safeTransferFrom(_msgSender(), owner(), principalOwed);
513: 
514:         loans[id].principalOwed = 0;
515:         loans[id].paymentDueBy = 0;
516:         loans[id].paymentsRemaining = 0;
517:         loans[id].state = LoanState.Repaid;
518:     }

This allows her to repay only the interest for one payment interval and the 14-day late fee, rather than the full interest and late fees owed over the three missed payments. This action by Alice results in a financial loss to the protocol. This issue arises because the amountOwed function does not account for the potential scenario where there are multiple missed payments during the grace period.

Impact

By exploiting the callLoan function, borrowers can circumvent the full payment of fees owed to the protocol, resulting in financial losses for the protocol.

Tool used

Manual Review

Code Snippet

https://github.com/sherlock-audit/2024-03-zivoe/blob/d4111645b19a1ad3ccc899bea073b6f19be04ccd/zivoe-core-foundry/src/lockers/OCC/OCC_Modular.sol#L500

Recommendation

In the callLoan function, it is recommended to account for and require payment of any missed interest and late fees that occurred during the delinquency period

Duplicate of #97

@github-actions github-actions bot closed this as completed May 5, 2024
@github-actions github-actions bot added Medium A valid Medium severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels May 5, 2024
@sherlock-admin2
Copy link
Contributor

1 comment(s) were left on this issue during the judging contest.

panprog commented:

medium, dup of #97, if there are more than 1 interest payments missed by borrower, then callLoan takes only 1 period payment, allowing borrower to skip paying the other periods interest and lateFee payments.

@sherlock-admin2 sherlock-admin2 changed the title Decent Chiffon Wolf - Borrowers can circumvent fees by calling OCC_Modular::callLoan when the grace period exceeds the payment interval KupiaSec - Borrowers can circumvent fees by calling OCC_Modular::callLoan when the grace period exceeds the payment interval May 11, 2024
@sherlock-admin2 sherlock-admin2 added the Reward A payout will be made for this issue label May 11, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

2 participants