diff --git a/crates/loans/src/interest.rs b/crates/loans/src/interest.rs index 7f196cadd1..c0468383ea 100644 --- a/crates/loans/src/interest.rs +++ b/crates/loans/src/interest.rs @@ -129,8 +129,9 @@ impl Pallet { } /// Calculate the borrowing utilization ratio of the specified market - /// - /// utilizationRatio = totalBorrows / (totalCash + totalBorrows − totalReserves) + /// `utilization_ratio = borrows / (cash + borrows - reserve)` + /// Since the market can reach a state where `cash == 0 && borrows == reserve`, + /// this rate can be infinitely large. pub(crate) fn calc_utilization_ratio( cash: BalanceOf, borrows: BalanceOf, @@ -145,6 +146,9 @@ impl Pallet { .and_then(|r| r.checked_sub(reserves)) .ok_or(ArithmeticError::Overflow)?; + if total.is_zero() { + return Err(DispatchError::Arithmetic(ArithmeticError::DivisionByZero)); + } Ok(Ratio::from_rational(borrows, total)) } diff --git a/crates/loans/src/lib.rs b/crates/loans/src/lib.rs index 8f4373ffa4..e5fc4e8936 100644 --- a/crates/loans/src/lib.rs +++ b/crates/loans/src/lib.rs @@ -1195,13 +1195,12 @@ pub mod pallet { Ok(().into()) } - /// Add reserves by transferring from payer. - /// TODO: This extrinsic currently does nothing useful. See the TODO comment - /// of the `ensure_enough_cash` function for more details. Based on that - /// TODO, decide whether this extrinsic should be kept. + /// Add reserves by transferring from payer. This is useful because the reserve balance can be used as + /// cash for redeeming. In case the market reaches 100% utilization, the protocol can deposit + /// more cash to the reserve, to allow users to exit (redeem) their lend token positions. + /// The reserve cash cannot be used for borrowing. /// /// May only be called from `T::ReserveOrigin`. - /// /// - `payer`: the payer account. /// - `asset_id`: the assets to be added. /// - `add_amount`: the amount to be added. @@ -1454,7 +1453,9 @@ impl Pallet { // Ensure there is enough cash in the market let exchange_rate = Self::exchange_rate_stored(asset_id)?; let redeem_amount = Self::calc_underlying_amount(voucher_amount, exchange_rate)?; - Self::ensure_enough_cash(asset_id, redeem_amount)?; + if Self::get_total_cash(asset_id) < redeem_amount { + return Err(Error::::InsufficientCash.into()); + } // TODO: Only free lend tokens are redeemable. Replace logic below with this: // if voucher_amount > Self::free_lend_tokens(asset_id, redeemer)?.amount() { @@ -1511,7 +1512,7 @@ impl Pallet { /// Borrower shouldn't borrow more than their total collateral value allows fn borrow_allowed(asset_id: AssetIdOf, borrower: &T::AccountId, borrow_amount: BalanceOf) -> DispatchResult { Self::ensure_under_borrow_cap(asset_id, borrow_amount)?; - Self::ensure_enough_cash(asset_id, borrow_amount)?; + Self::ensure_borrow_cash(asset_id, borrow_amount)?; let borrow_value = Self::get_asset_value(asset_id, borrow_amount)?; Self::ensure_liquidity(borrower, borrow_value)?; @@ -1833,7 +1834,7 @@ impl Pallet { /// https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/CToken.sol#L518 /// - but getCashPrior is the entire balance of the contract: /// https://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/CToken.sol#L1125 - fn ensure_enough_cash(asset_id: AssetIdOf, amount: BalanceOf) -> DispatchResult { + fn ensure_borrow_cash(asset_id: AssetIdOf, amount: BalanceOf) -> DispatchResult { let reducible_cash = Self::get_total_cash(asset_id) .checked_sub(Self::total_reserves(asset_id)) .ok_or(ArithmeticError::Underflow)?; diff --git a/crates/loans/src/tests.rs b/crates/loans/src/tests.rs index 91e3d24320..34be7bddae 100644 --- a/crates/loans/src/tests.rs +++ b/crates/loans/src/tests.rs @@ -362,7 +362,7 @@ fn redeem_fails_when_insufficient_liquidity() { } #[test] -fn redeem_fails_when_would_use_reserved_balanace() { +fn redeem_succeds_when_would_use_reserved_balanace() { new_test_ext().execute_with(|| { // Prepare: Bob Deposit 200 DOT assert_ok!(Loans::mint(RuntimeOrigin::signed(BOB), DOT, 200)); @@ -375,10 +375,7 @@ fn redeem_fails_when_would_use_reserved_balanace() { assert_ok!(Loans::borrow(RuntimeOrigin::signed(ALICE), DOT, 50)); assert_ok!(Loans::add_reserves(RuntimeOrigin::root(), ALICE, DOT, 50)); - assert_noop!( - Loans::redeem(RuntimeOrigin::signed(BOB), DOT, 151), - Error::::InsufficientCash - ); + assert_ok!(Loans::redeem(RuntimeOrigin::signed(BOB), DOT, 151)); }) } @@ -956,7 +953,7 @@ fn calc_collateral_amount_works() { } #[test] -fn ensure_enough_cash_works() { +fn ensure_borrow_cash_works() { new_test_ext().execute_with(|| { assert_ok!(Tokens::set_balance( RuntimeOrigin::root(), @@ -965,17 +962,17 @@ fn ensure_enough_cash_works() { unit(1000), 0 )); - assert_ok!(Loans::ensure_enough_cash(KSM, unit(1000))); + assert_ok!(Loans::ensure_borrow_cash(KSM, unit(1000))); TotalReserves::::insert(KSM, unit(10)); assert_noop!( - Loans::ensure_enough_cash(KSM, unit(1000)), + Loans::ensure_borrow_cash(KSM, unit(1000)), Error::::InsufficientCash, ); - assert_ok!(Loans::ensure_enough_cash(KSM, unit(990))); + assert_ok!(Loans::ensure_borrow_cash(KSM, unit(990))); // Borrows don't count as cash TotalBorrows::::insert(KSM, unit(20)); assert_noop!( - Loans::ensure_enough_cash(KSM, unit(1000)), + Loans::ensure_borrow_cash(KSM, unit(1000)), Error::::InsufficientCash ); })