Audit finding: Users can avoid accruing interest by repaying a zero amount
Project: Parallel
Auditor: Trail of Bits
Location: pallets/loans/src/lib.rs:1057-1087,1106-1121
Impact: Medium
Likelihood: Difficulty-Low
Vulnerability class (audit): Data Validation
Vulnerability class (Scout): Business Logic

2. Users can avoid accruing interest by repaying a zero amount
Severity: Medium
Difficulty: Low
Type: Data Validation
Finding ID: TOB-PLF-2
Target: pallets/loans/src/lib.rs
Description
To repay borrowed funds, users call the repay_borrow extrinsic. The extrinsic implementation calls the Pallet::repay_borrow_internal method to recompute the loan balance. Pallet::repay_borrow_internal updates the loan balance for the account and resets the borrow index as part of the calculation.
fn repay_borrow_internal(
borrower: &T::AccountId,
asset_id: AssetIdOf<T>,
account_borrows: BalanceOf<T>,
repay_amount: BalanceOf<T>,
) -> DispatchResult {
// ... <redacted>
AccountBorrows::<T>::insert(
asset_id,
borrower,
BorrowSnapshot {
principal: account_borrows_new,
borrow_index: Self::borrow_index(asset_id),
},
);
TotalBorrows::<T>::insert(asset_id, total_borrows_new);
Ok(())
}
Figure 2.1: pallets/loans/src/lib.rs:1057-1087
The borrow index is used in the calculation of the accumulated interest for the loan in Pallet::current_balance_from_snapshot. Specifically, the outstanding balance, snapshot.principal, is multiplied by the quotient of borrow_index divided by snapshot.borrow_index.
pub fn current_balance_from_snapshot(
asset_id: AssetIdOf<T>,
snapshot: BorrowSnapshot<BalanceOf<T>>,
) -> Result<BalanceOf<T>, DispatchError> {
if snapshot.principal.is_zero() || snapshot.borrow_index.is_zero() {
return Ok(Zero::zero());
}
// Calculate new borrow balance using the interest index:
// recent_borrow_balance = snapshot.principal * borrow_index /
// snapshot.borrow_index
let recent_borrow_balance = Self::borrow_index(asset_id)
.checked_div(&snapshot.borrow_index)
.and_then(|r| r.checked_mul_int(snapshot.principal))
.ok_or(ArithmeticError::Overflow)?;
Ok(recent_borrow_balance)
}
Figure 2.2: pallets/loans/src/lib.rs:1106-1121
Therefore, if the snapshot borrow index is updated to Self::borrow_index(asset_id), the resulting recent_borrow_balance in Pallet::current_balance_from_snapshot will always be equal to snapshot.principal. That is, no interest will be applied to the loan. It follows that the accrued interest is lost whenever part of the loan is repaid. In an extreme case, if the repaid amount passed to repay_borrow is 0, users could reset the borrow index without repaying anything.
The same issue is present in the implementations of the liquidated_transfer and borrow extrinsics as well.
Exploit Scenario
A malicious user borrows assets from Parallel Finance and calls repay_borrow with a repay_amount of zero. This allows her to avoid paying interest on the loan.
Recommendations
Short term, modify the code so that the accrued interest is added to the snapshot principal when the snapshot is updated.
Long term, add unit tests for edge cases (like repaying a zero amount) to increase the chances of discovering unexpected system behavior.

Summary: Users can avoid paying interest on loans by repaying a zero amount, resetting the borrow index without repaying anything.