Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions lending_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum PoolError {
NoProposedAdmin = 10,
CooldownTooLong = 11,
NotPaused = 12,
WithdrawalCooldownActive = 13,
}

/// Storage keys.
Expand Down Expand Up @@ -231,20 +232,26 @@ impl LendingPool {
.ok_or(PoolError::InvalidAmount)
}

fn assert_withdrawal_cooldown_elapsed(env: &Env, provider: &Address, token: &Address) {
fn assert_withdrawal_cooldown_elapsed(
env: &Env,
provider: &Address,
token: &Address,
) -> Result<(), PoolError> {
let cooldown = Self::withdrawal_cooldown(env);
if cooldown == 0 {
return;
return Ok(());
}

let Some(deposit_ledger) = Self::read_deposit_timestamp(env, provider, token) else {
return;
return Ok(());
};

let current_ledger = env.ledger().sequence();
if current_ledger < deposit_ledger.saturating_add(cooldown) {
panic!("withdrawal_cooldown_active");
return Err(PoolError::WithdrawalCooldownActive);
}

Ok(())
}

/// Burns `shares` for `provider` and transfers out the proportional
Expand Down Expand Up @@ -608,7 +615,7 @@ impl LendingPool {
) -> Result<(), PoolError> {
provider.require_auth();
Self::assert_not_paused(&env)?;
Self::assert_withdrawal_cooldown_elapsed(&env, &provider, &token);
Self::assert_withdrawal_cooldown_elapsed(&env, &provider, &token)?;
let assets = Self::redeem_shares(&env, &provider, &token, shares)?;
withdraw(&env, provider, token, assets, shares);
Ok(())
Expand Down
6 changes: 3 additions & 3 deletions lending_pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ fn test_insufficient_balance_withdraw_panic() {
}

#[test]
#[should_panic(expected = "withdrawal_cooldown_active")]
fn test_immediate_withdraw_panics_when_cooldown_active() {
fn test_withdraw_returns_cooldown_error_when_cooldown_active() {
let env = Env::default();
env.mock_all_auths();

Expand All @@ -210,7 +209,8 @@ fn test_immediate_withdraw_panics_when_cooldown_active() {
stellar_asset_client.mint(&provider, &5_000);
pool_client.deposit(&provider, &token_id, &1_000);

pool_client.withdraw(&provider, &token_id, &1_000);
let result = pool_client.try_withdraw(&provider, &token_id, &1_000);
assert_eq!(result, Err(Ok(crate::PoolError::WithdrawalCooldownActive)));
}

#[test]
Expand Down
15 changes: 11 additions & 4 deletions loan_manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub enum LoanError {
InvalidExtension = 25,
InsufficientCollateral = 26,
LoanNotLiquidatable = 27,
RepaymentBelowMinimum = 28,
}

#[contracttype]
Expand Down Expand Up @@ -513,6 +514,10 @@ impl LoanManager {
.checked_add(delta)
.expect("total outstanding overflow");

// This can only become negative if the contract state is inconsistent:
// repayment logic only ever subtracts the exact principal amount of a
// fully repaid loan, and the corresponding loan-approval bookkeeping
// prevents a second subtraction from the same outstanding balance.
if updated < 0 {
panic!("total outstanding underflow");
}
Expand Down Expand Up @@ -1246,7 +1251,7 @@ impl LoanManager {
let is_rounding_dust_forgiveness = total_debt <= min_repayment_amount;

if amount < total_debt && amount < min_repayment_amount && !is_rounding_dust_forgiveness {
panic!("repayment amount below minimum");
return Err(LoanError::RepaymentBelowMinimum);
}

let token: Address = env
Expand Down Expand Up @@ -2012,16 +2017,16 @@ impl LoanManager {
Self::max_loan_amount(&env)
}

pub fn set_min_repayment_amount(env: Env, amount: i128) {
pub fn set_min_repayment_amount(env: Env, amount: i128) -> Result<(), LoanError> {
if amount < 0 {
panic!("min repayment amount cannot be negative");
return Err(LoanError::InvalidAmount);
}

let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.expect("not initialized");
.ok_or(LoanError::NotInitialized)?;
admin.require_auth();

let old_amount = Self::min_repayment_amount(&env);
Expand All @@ -2030,6 +2035,8 @@ impl LoanManager {
.set(&DataKey::MinRepaymentAmount, &amount);
Self::bump_instance_ttl(&env);
events::min_repayment_updated(&env, admin, old_amount, amount);

Ok(())
}

pub fn get_min_repayment_amount(env: Env) -> i128 {
Expand Down
15 changes: 13 additions & 2 deletions loan_manager/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,6 @@ fn test_partial_repayment_tracks_split_balances() {
}

#[test]
#[should_panic(expected = "repayment amount below minimum")]
fn test_minimum_repayment_amount_enforced() {
let env = Env::default();
env.mock_all_auths_allowing_non_root_auth();
Expand All @@ -704,7 +703,19 @@ fn test_minimum_repayment_amount_enforced() {
manager.approve_loan(&loan_id);

manager.set_min_repayment_amount(&150);
manager.repay(&borrower, &loan_id, &100);
let result = manager.try_repay(&borrower, &loan_id, &100);
assert_eq!(result, Err(Ok(LoanError::RepaymentBelowMinimum)));
}

#[test]
fn test_set_min_repayment_amount_rejects_negative_values() {
let env = Env::default();
env.mock_all_auths();

let (manager, _nft_client, _pool_client, _token_id, _token_admin) = setup_test(&env);

let result = manager.try_set_min_repayment_amount(&-1);
assert_eq!(result, Err(Ok(LoanError::InvalidAmount)));
}

#[test]
Expand Down
Loading