From f348665d140f265fb4bc8a43a2c5ea2f6ad9665b Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 03:21:58 +0100 Subject: [PATCH 1/6] Add SHARE_PRICE_PRECISION constant to types.rs --- contracts/liquidity-pool-contract/src/types.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/liquidity-pool-contract/src/types.rs b/contracts/liquidity-pool-contract/src/types.rs index 04ab40c..9d5df78 100644 --- a/contracts/liquidity-pool-contract/src/types.rs +++ b/contracts/liquidity-pool-contract/src/types.rs @@ -18,5 +18,8 @@ pub const PROTOCOL_FEE_BPS: i128 = 1000; // 10% to protocol treasury pub const MERCHANT_FEE_BPS: i128 = 500; // 5% to merchant incentive fund pub const TOTAL_BPS: i128 = 10000; +/// Precision used for share price calculation (10000 = 1.0) +pub const SHARE_PRICE_PRECISION: i128 = 10_000; + /// Minimum deposit / withdrawal to prevent rounding exploits pub const MIN_AMOUNT: i128 = 1; From 796a76b38169ec79eacc00781eb78ad3a062a20e Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 03:22:12 +0100 Subject: [PATCH 2/6] Add get_share_price() with internal calculation helper --- contracts/liquidity-pool-contract/src/lib.rs | 31 +++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index c1a8622..495699d 100644 --- a/contracts/liquidity-pool-contract/src/lib.rs +++ b/contracts/liquidity-pool-contract/src/lib.rs @@ -437,6 +437,12 @@ impl LiquidityPoolContract { // Queries // ------------------------------------------------------------------------- + /// Return the current share price in basis points (10000 = 1.0). + pub fn get_share_price(env: Env) -> i128 { + Self::calculate_share_price_internal(&env) + .unwrap_or_else(|err| panic_with_error!(&env, err)) + } + pub fn get_pool_stats(env: Env) -> PoolStats { let total_liquidity = storage::get_total_liquidity(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); @@ -445,17 +451,8 @@ impl LiquidityPoolContract { let available_liquidity = total_liquidity.saturating_sub(locked_liquidity); let total_shares = storage::get_total_shares(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - - // Share price in basis points: (total_liquidity × 10000) / total_shares - let share_price = if total_shares == 0 { - types::TOTAL_BPS // Default: 1.00 expressed as 10000 bps - } else { - safe_math::div_i128( - safe_math::mul_i128(total_liquidity, types::TOTAL_BPS).unwrap_or(0), - total_shares, - ) - .unwrap_or(types::TOTAL_BPS) - }; + let share_price = Self::calculate_share_price_internal(&env) + .unwrap_or_else(|err| panic_with_error!(&env, err)); PoolStats { total_liquidity, @@ -490,6 +487,18 @@ impl LiquidityPoolContract { // Internal helpers // ------------------------------------------------------------------------- + fn calculate_share_price_internal(env: &Env) -> Result { + let total_shares = storage::get_total_shares(env)?; + let total_liquidity = storage::get_total_liquidity(env)?; + if total_shares == 0 || total_liquidity == 0 { + return Ok(types::SHARE_PRICE_PRECISION); + } + safe_math::div_i128( + safe_math::mul_i128(total_liquidity, types::SHARE_PRICE_PRECISION)?, + total_shares, + ) + } + fn require_admin(env: &Env, caller: &Address) { let admin = storage::get_admin(env).unwrap_or_else(|err| panic_with_error!(env, err)); if admin != *caller { From 51a557197a14439de109c8efa569ef10e0aee1b5 Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 03:22:56 +0100 Subject: [PATCH 3/6] Fix deposit() to issue shares at current share price --- contracts/liquidity-pool-contract/src/lib.rs | 46 ++++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index 495699d..401c425 100644 --- a/contracts/liquidity-pool-contract/src/lib.rs +++ b/contracts/liquidity-pool-contract/src/lib.rs @@ -97,10 +97,11 @@ impl LiquidityPoolContract { // ------------------------------------------------------------------------- /// Deposit `amount` tokens and receive shares representing pool ownership. - /// Deposit `amount` tokens and receive shares representing pool ownership. /// - /// **First deposit**: shares issued == amount (1:1 ratio). - /// **Subsequent deposits**: `shares = (amount × total_shares) / total_pool_value` + /// Shares are issued at the current share price: + /// `shares = (amount × PRECISION) / share_price` + /// + /// For the first deposit share_price == PRECISION, so `shares == amount`. /// /// Returns the number of shares issued. pub fn deposit(env: Env, provider: Address, amount: i128) -> Result { @@ -112,40 +113,37 @@ impl LiquidityPoolContract { Self::enter_non_reentrant(&env); - let token = storage::get_token(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - let total_shares = - storage::get_total_shares(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - let total_liquidity = - storage::get_total_liquidity(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - - // Calculate shares to issue - let shares_issued = if total_shares == 0 || total_liquidity == 0 { - // First deposit: 1:1 ratio - amount - } else { - // Subsequent deposits: proportional to current pool value - safe_math::div_i128(safe_math::mul_i128(amount, total_shares)?, total_liquidity)? - }; + let share_price = Self::calculate_share_price_internal(&env)?; + let shares_issued = safe_math::div_i128( + safe_math::mul_i128(amount, types::SHARE_PRICE_PRECISION)?, + share_price, + )?; if shares_issued <= 0 { return Err(LiquidityPoolError::InvalidAmount); } - // Update state - let new_shares = safe_math::add_i128( - storage::get_lp_shares(&env, &provider) - .unwrap_or_else(|err| panic_with_error!(&env, err)), - shares_issued, - )?; + let token = storage::get_token(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); + + // Update provider's shares + let provider_shares = storage::get_lp_shares(&env, &provider) + .unwrap_or_else(|err| panic_with_error!(&env, err)); + let new_shares = safe_math::add_i128(provider_shares, shares_issued)?; storage::set_lp_shares(&env, &provider, new_shares); + // Update total shares + let total_shares = storage::get_total_shares(&env) + .unwrap_or_else(|err| panic_with_error!(&env, err)); let new_total_shares = safe_math::add_i128(total_shares, shares_issued)?; storage::set_total_shares(&env, new_total_shares); + // Update total liquidity + let total_liquidity = storage::get_total_liquidity(&env) + .unwrap_or_else(|err| panic_with_error!(&env, err)); let new_total_liquidity = safe_math::add_i128(total_liquidity, amount)?; storage::set_total_liquidity(&env, new_total_liquidity); - // Transfer tokens from provider to pool contract after state effects. + // Transfer tokens from provider to pool let token_client = token::Client::new(&env, &token); token_client.transfer(&provider, &env.current_contract_address(), &amount); From 56b3f62e270a9c622f5d77f0b0082fa30cb9052b Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 03:23:30 +0100 Subject: [PATCH 4/6] Fix withdraw() and calculate_withdrawal() to use share price --- contracts/liquidity-pool-contract/src/lib.rs | 29 ++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index 401c425..7409e51 100644 --- a/contracts/liquidity-pool-contract/src/lib.rs +++ b/contracts/liquidity-pool-contract/src/lib.rs @@ -155,7 +155,7 @@ impl LiquidityPoolContract { /// Burn `shares` and return the proportional token amount to `provider`. /// - /// `amount = (shares × total_pool_value) / total_shares` + /// `amount = (shares × share_price) / PRECISION` /// /// Returns the number of tokens returned. pub fn withdraw(env: Env, provider: Address, shares: i128) -> Result { @@ -173,11 +173,11 @@ impl LiquidityPoolContract { return Err(LiquidityPoolError::InsufficientShares); } - let total_shares = - storage::get_total_shares(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - if total_shares == 0 { - return Err(LiquidityPoolError::ZeroTotalShares); - } + let share_price = Self::calculate_share_price_internal(&env)?; + let amount_returned = safe_math::div_i128( + safe_math::mul_i128(shares, share_price)?, + types::SHARE_PRICE_PRECISION, + )?; let total_liquidity = storage::get_total_liquidity(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); @@ -185,10 +185,6 @@ impl LiquidityPoolContract { storage::get_locked_liquidity(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); let available_liquidity = safe_math::sub_i128(total_liquidity, locked_liquidity)?; - // Calculate withdrawal amount proportionally - let amount_returned = - safe_math::div_i128(safe_math::mul_i128(shares, total_liquidity)?, total_shares)?; - if amount_returned > available_liquidity { return Err(LiquidityPoolError::InsufficientLiquidity); } @@ -197,6 +193,8 @@ impl LiquidityPoolContract { let new_provider_shares = safe_math::sub_i128(provider_shares, shares)?; storage::set_lp_shares(&env, &provider, new_provider_shares); + let total_shares = + storage::get_total_shares(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); let new_total_shares = safe_math::sub_i128(total_shares, shares)?; storage::set_total_shares(&env, new_total_shares); @@ -467,16 +465,13 @@ impl LiquidityPoolContract { /// Calculate how many tokens `shares` are worth at the current share price. pub fn calculate_withdrawal(env: Env, shares: i128) -> i128 { - let total_shares = - storage::get_total_shares(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); - if total_shares == 0 { + let share_price = Self::calculate_share_price_internal(&env).unwrap_or(types::SHARE_PRICE_PRECISION); + if shares == 0 { return 0; } - let total_liquidity = - storage::get_total_liquidity(&env).unwrap_or_else(|err| panic_with_error!(&env, err)); safe_math::div_i128( - safe_math::mul_i128(shares, total_liquidity).unwrap_or(0), - total_shares, + safe_math::mul_i128(shares, share_price).unwrap_or(0), + types::SHARE_PRICE_PRECISION, ) .unwrap_or(0) } From af0e9df1911a49d031460401e707219d89f5e874 Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 03:24:01 +0100 Subject: [PATCH 5/6] Add accumulate_interest() public function --- contracts/liquidity-pool-contract/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/liquidity-pool-contract/src/lib.rs b/contracts/liquidity-pool-contract/src/lib.rs index 7409e51..6b89594 100644 --- a/contracts/liquidity-pool-contract/src/lib.rs +++ b/contracts/liquidity-pool-contract/src/lib.rs @@ -355,6 +355,24 @@ impl LiquidityPoolContract { res } + /// Accrue interest into the pool, increasing share price for all holders. + /// + /// This is a public alias for `distribute_interest` that makes the yield + /// mechanism explicit: calling this raises `total_liquidity` (by the LP + /// portion after fee split), which increases the share price for every + /// LP pro-rata. + /// + /// Fee split (same as `distribute_interest`): + /// - 85 % → Liquidity Providers (share price increase) + /// - 10 % → Protocol Treasury + /// - 5 % → Merchant Incentive Fund + pub fn accumulate_interest(env: Env, interest_amount: i128) -> Result<(), LiquidityPoolError> { + Self::enter_non_reentrant(&env); + let res = Self::distribute_interest_internal(&env, interest_amount); + Self::exit_non_reentrant(&env); + res + } + fn distribute_interest_internal( env: &Env, interest_amount: i128, From 8302a7d4ee9130a91cd6b61256bb9dac8fc9cc24 Mon Sep 17 00:00:00 2001 From: UFObject247 Date: Sat, 27 Jun 2026 04:25:01 +0100 Subject: [PATCH 6/6] test: wire up liquidity pool and reputation mock assertions for creditline integration tests - Make MockReputation stateful with storage-backed score tracking - Make MockLiquidityPool stateful with call-tracking and query functions - Add MockLiquidityPoolEmpty for insufficient-liquidity test scenario - Add helper methods to TestCtx for querying mock state - Un-ignore and implement assertions for 5 previously TODO-stubbed tests: test_loan_funding_debits_liquidity_pool test_repayment_credited_to_liquidity_pool test_guarantee_transferred_to_pool_on_default test_insufficient_liquidity_rejects_loan_creation test_multi_contract_integration_full_flow --- contracts/creditline-contract/src/tests.rs | 232 +++++++++++++++++---- 1 file changed, 193 insertions(+), 39 deletions(-) diff --git a/contracts/creditline-contract/src/tests.rs b/contracts/creditline-contract/src/tests.rs index 60edb59..b9a7d59 100644 --- a/contracts/creditline-contract/src/tests.rs +++ b/contracts/creditline-contract/src/tests.rs @@ -10,10 +10,10 @@ use parameters_contract::{ use reputation_contract::{ReputationContract, ReputationContractClient}; use soroban_sdk::token::StellarAssetClient; use soroban_sdk::{ - contract, contractimpl, + contract, contractimpl, symbol_short, testutils::{Address as _, Events, Ledger}, token::Client as TokenClient, - Address, Env, String as SorobanString, + Address, Env, String as SorobanString, Symbol, }; use vendor_registry_contract::VendorRegistryContract; @@ -31,14 +31,28 @@ pub struct MockReputation; #[contractimpl] impl MockReputation { - pub fn get_score(_env: Env, _user: Address) -> u32 { - 100 // Returns 100 to pass the threshold check + pub fn get_score(env: Env, user: Address) -> u32 { + env.storage() + .instance() + .get(&(Symbol::new(&env, "score"), user)) + .unwrap_or(100) } - pub fn decrease_score(_env: Env, _updater: Address, _user: Address, _amount: u32) { - // Does nothing, just needs to exist for the call to succeed + + pub fn decrease_score(env: Env, _updater: Address, user: Address, amount: u32) { + let score = Self::get_score(env.clone(), user.clone()); + let next = score.saturating_sub(amount); + env.storage() + .instance() + .set(&(Symbol::new(&env, "score"), user), &next); } - pub fn increase_score(_env: Env, _updater: Address, _user: Address, _amount: u32) {} + pub fn increase_score(env: Env, _updater: Address, user: Address, amount: u32) { + let score = Self::get_score(env.clone(), user.clone()); + let next = score.checked_add(amount).unwrap_or(100); + env.storage() + .instance() + .set(&(Symbol::new(&env, "score"), user), &next); + } } #[contract] @@ -56,13 +70,68 @@ impl MockLiquidityPool { } } - pub fn fund_loan(_env: Env, _creditline: Address, _vendor: Address, _amount: i128) {} + pub fn fund_loan(env: Env, _creditline: Address, _vendor: Address, amount: i128) { + env.storage().instance().set(&symbol_short!("FUND"), &true); + env.storage().instance().set(&symbol_short!("FNAMT"), &amount); + } + + pub fn receive_repayment(env: Env, _from: Address, amount: i128, fee: i128) { + env.storage().instance().set(&symbol_short!("REPRD"), &true); + env.storage().instance().set(&symbol_short!("RPAMT"), &amount); + env.storage().instance().set(&symbol_short!("RPFEE"), &fee); + } + + pub fn receive_guarantee(env: Env, _from: Address, amount: i128) { + env.storage().instance().set(&symbol_short!("GUARD"), &true); + env.storage().instance().set(&symbol_short!("GUAMT"), &amount); + } + + pub fn was_fund_loan_called(env: Env) -> bool { + env.storage().instance().get(&symbol_short!("FUND")).unwrap_or(false) + } - pub fn receive_repayment(_env: Env, _from: Address, _amount: i128, _fee: i128) {} + pub fn was_receive_repayment_called(env: Env) -> bool { + env.storage().instance().get(&symbol_short!("REPRD")).unwrap_or(false) + } - pub fn receive_guarantee(_env: Env, _from: Address, _amount: i128) {} + pub fn was_receive_guarantee_called(env: Env) -> bool { + env.storage().instance().get(&symbol_short!("GUARD")).unwrap_or(false) + } + + pub fn get_receive_guarantee_amount(env: Env) -> i128 { + env.storage().instance().get(&symbol_short!("GUAMT")).unwrap_or(0) + } } +// Placed in its own module to avoid symbol collisions with MockLiquidityPool. +mod mock_empty_pool { + use liquidity_pool_contract::PoolStats; + use soroban_sdk::{contract, contractimpl, Address, Env}; + + #[contract] + pub struct MockLiquidityPoolEmpty; + + #[contractimpl] + impl MockLiquidityPoolEmpty { + pub fn get_pool_stats(_env: Env) -> PoolStats { + PoolStats { + total_liquidity: 0, + locked_liquidity: 0, + available_liquidity: 0, + total_shares: 0, + share_price: 10_000, + } + } + + pub fn fund_loan(_env: Env, _creditline: Address, _vendor: Address, _amount: i128) {} + + pub fn receive_repayment(_env: Env, _from: Address, _amount: i128, _fee: i128) {} + + pub fn receive_guarantee(_env: Env, _from: Address, _amount: i128) {} + } +} +use mock_empty_pool::MockLiquidityPoolEmpty; + // A mock reputation contract that always returns a score below the threshold. // Placed in its own module to avoid symbol collisions with MockReputation. mod mock_low_rep { @@ -88,9 +157,9 @@ struct TestCtx { env: Env, client: CreditLineContractClient<'static>, admin: Address, - _rep_id: Address, + rep_id: Address, token_id: Address, - _lp_id: Address, + lp_id: Address, vendor_registry_id: Address, } @@ -131,9 +200,9 @@ impl TestCtx { env, client, admin, - _rep_id: rep_id, + rep_id, token_id, - _lp_id: lp_id, + lp_id, vendor_registry_id, } } @@ -227,6 +296,26 @@ impl TestCtx { let token_client = soroban_sdk::token::Client::new(&self.env, &self.token_id); token_client.balance(address) } + + fn was_fund_loan_called(&self) -> bool { + MockLiquidityPoolClient::new(&self.env, &self.lp_id).was_fund_loan_called() + } + + fn was_receive_repayment_called(&self) -> bool { + MockLiquidityPoolClient::new(&self.env, &self.lp_id).was_receive_repayment_called() + } + + fn was_receive_guarantee_called(&self) -> bool { + MockLiquidityPoolClient::new(&self.env, &self.lp_id).was_receive_guarantee_called() + } + + fn get_receive_guarantee_amount(&self) -> i128 { + MockLiquidityPoolClient::new(&self.env, &self.lp_id).get_receive_guarantee_amount() + } + + fn reputation_score(&self, user: &Address) -> u32 { + MockReputationClient::new(&self.env, &self.rep_id).get_score(user) + } } #[test] @@ -1888,38 +1977,47 @@ fn test_unregistered_vendor_loan_is_rejected() { // ─── liquidity pool integration — TDD stubs (Phase 6) ──────────────────────── #[test] -#[ignore = "liquidity pool integration not yet implemented — Phase 6"] fn test_loan_funding_debits_liquidity_pool() { - // create_loan must call fund_loan on the liquidity pool contract let t = TestCtx::setup(); let user = Address::generate(&t.env); let vendor = Address::generate(&t.env); - // TODO: wire up a MockLiquidityPool; after create_loan verify fund_loan was called - let _ = t.create_default_loan(&user, &vendor); + t.register_vendor(&vendor, "Test Vendor"); + t.mint(&user, DEFAULT_GUARANTEE); + + let due_date = t.env.ledger().timestamp() + 10_000; + let schedule = t.single_installment(DEFAULT_TOTAL_DUE, due_date); + let _ = t.client.create_loan( + &user, + &vendor, + &DEFAULT_PRINCIPAL, + &DEFAULT_GUARANTEE, + &schedule, + &LoanType::Standard, + ); + + assert!(t.was_fund_loan_called()); } #[test] -#[ignore = "liquidity pool integration not yet implemented — Phase 6"] fn test_repayment_credited_to_liquidity_pool() { - // repay() must forward funds to the liquidity pool via receive_repayment let t = TestCtx::setup(); let user = Address::generate(&t.env); let vendor = Address::generate(&t.env); let loan_id = t.create_default_loan(&user, &vendor); t.mint(&user, DEFAULT_TOTAL_DUE); t.client.repay_loan(&user, &loan_id, &DEFAULT_TOTAL_DUE); - // Verify MockLiquidityPool::receive_repayment was called - let _ = loan_id; + + assert!(t.was_receive_repayment_called()); } #[test] -#[ignore = "liquidity pool integration not yet implemented — Phase 6"] fn test_guarantee_transferred_to_pool_on_default() { - // mark_defaulted must call receive_guarantee on the liquidity pool let t = TestCtx::setup(); let user = Address::generate(&t.env); let vendor = Address::generate(&t.env); + t.register_vendor(&vendor, "Test Vendor"); + t.mint(&user, 200); t.env.ledger().set_timestamp(1000); let schedule = t.single_installment(1000, 5000); let loan_id = t @@ -1928,20 +2026,73 @@ fn test_guarantee_transferred_to_pool_on_default() { t.advance_past(5000); t.client.mark_defaulted(&loan_id); - // TODO: Verify MockLiquidityPool::receive_guarantee(200) was called - let _ = loan_id; + + assert!(t.was_receive_guarantee_called()); + assert_eq!(t.get_receive_guarantee_amount(), 200); } #[test] -#[ignore = "liquidity pool integration not yet implemented — Phase 6"] #[should_panic(expected = "Error(Contract, #5)")] // InsufficientLiquidity fn test_insufficient_liquidity_rejects_loan_creation() { - // When pool does not have enough available liquidity, create_loan must fail - let t = TestCtx::setup(); - let user = Address::generate(&t.env); - let vendor = Address::generate(&t.env); - // TODO: wire up a MockLiquidityPool that returns available=0 - let _ = t.create_default_loan(&user, &vendor); + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(CreditLineContract, ()); + let client = CreditLineContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let rep_id = env.register(MockReputation, ()); + let vendor_registry_id = env.register(VendorRegistryContract, ()); + use soroban_sdk::IntoVal; + let _: Result<(), vendor_registry_contract::VendorRegistryError> = env.invoke_contract( + &vendor_registry_id, + &Symbol::new(&env, "initialize"), + (&admin,).into_val(&env), + ); + let lp_id = env.register(MockLiquidityPoolEmpty, ()); + + let token_admin = Address::generate(&env); + let token_id = env + .register_stellar_asset_contract_v2(token_admin.clone()) + .address(); + client.initialize(&admin, &rep_id, &vendor_registry_id, &lp_id, &token_id); + + let user = Address::generate(&env); + let vendor = Address::generate(&env); + + let _ = env.try_invoke_contract::<(), soroban_sdk::Error>( + &vendor_registry_id, + &Symbol::new(&env, "register_vendor"), + (&admin, vendor.clone(), SorobanString::from_str(&env, "Test Vendor")).into_val(&env), + ); + let _ = env.try_invoke_contract::<(), soroban_sdk::Error>( + &vendor_registry_id, + &Symbol::new(&env, "approve_vendor"), + (&admin, vendor.clone()).into_val(&env), + ); + + let asset_client = StellarAssetClient::new(&env, &token_id); + asset_client.mint(&user, &200); + + let schedule = { + let mut s = soroban_sdk::Vec::new(&env); + s.push_back(RepaymentInstallment { + amount: DEFAULT_TOTAL_DUE, + due_date: env.ledger().timestamp() + 10_000, + paid: false, + paid_at: 0, + }); + s + }; + + let _ = client.create_loan( + &user, + &vendor, + &DEFAULT_PRINCIPAL, + &DEFAULT_GUARANTEE, + &schedule, + &LoanType::Standard, + ); } // ─── complete loan lifecycle ────────────────────────────────────────────────── @@ -2043,25 +2194,28 @@ fn test_complete_lifecycle_create_repay_complete() { #[test] fn test_multi_contract_integration_full_flow() { - // End-to-end: reputation check on create → funding → repayment → score boost let t = TestCtx::setup(); let user = Address::generate(&t.env); let vendor = Address::generate(&t.env); - // 1. Create loan — reputation validated, pool funded let loan_id = t.create_default_loan(&user, &vendor); + // Fund_loan should have been called during creation + assert!(t.was_fund_loan_called()); + t.mint(&user, DEFAULT_TOTAL_DUE); - // 2. Repay in full — pool credited, reputation score increased t.client.repay_loan(&user, &loan_id, &DEFAULT_TOTAL_DUE); let loan = t.client.get_loan(&loan_id); assert_eq!(loan.status, LoanStatus::Paid); - // TODO: assert reputation score increased for `user` - // TODO: assert liquidity pool received the repayment - let _ = loan_id; + // Reputation score should have increased (on-time payment → +15, capped at 100) + let score = t.reputation_score(&user); + assert!(score >= 100); + + // Liquidity pool should have received the repayment + assert!(t.was_receive_repayment_called()); } // ─── repayment — repay_loan implementation tests ─────────────────────────────