diff --git a/contracts/risk-pool/src/lib.rs b/contracts/risk-pool/src/lib.rs index 9021c5f..ad60c33 100644 --- a/contracts/risk-pool/src/lib.rs +++ b/contracts/risk-pool/src/lib.rs @@ -13,11 +13,10 @@ //! v2 — full implementation; Risk Pool is now deployable and testable. #![no_std] extern crate alloc; -use alloc::string::ToString; use soroban_sdk::{ contract, contractimpl, contracttype, contracterror, panic_with_error, - token, Address, BytesN, Env, Symbol, Vec, + token, Address, Env, Symbol, Vec, }; pub mod types; @@ -47,12 +46,16 @@ enum StorageKey { TotalLocked, TotalShares, AccumulatedPremium, + AccumulatedBackstop, Status, LpPosition(Address), - LpList, + LpCount, + LpAddress(u32), Lock(u128), AdminWithdrawalRequest, PendingAdmin, + PolicyEngine, + ClaimsProcessor, } #[contracterror] @@ -71,10 +74,10 @@ pub enum Error { AlreadyReleased = 10, Undercollateralized = 11, PoolCapExceeded = 12, - TimelockPending = 13, - TimelockNotReady = 14, - NoPendingWithdrawal = 15, InsufficientShares = 13, + TimelockPending = 14, + TimelockNotReady = 15, + NoPendingWithdrawal = 16, } #[contract] @@ -90,6 +93,8 @@ impl RiskPool { treasury: Address, backstop: Address, category: Symbol, + policy_engine: Address, + claims_processor: Address, ) { if env.storage().instance().has(&StorageKey::Initialized) { panic_with_error!(&env, Error::AlreadyInitialized); @@ -128,29 +133,33 @@ impl RiskPool { } admin.require_auth(); - env.storage().instance().set(&StorageKey::Initialized, &true); - env.storage().instance().set(&StorageKey::Admin, &admin); - env.storage().instance().set(&StorageKey::UsdcToken, &usdc_token); - env.storage().instance().set(&StorageKey::Treasury, &treasury); - env.storage().instance().set(&StorageKey::Backstop, &backstop); - env.storage().instance().set(&StorageKey::Category, &category); -env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); - // No pending admin initially - env.storage().instance().set(&StorageKey::PendingAdmin, &Address::from_uint(&env, 0)); - env.storage().instance().set(&StorageKey::TotalLocked, &0i128); - env.storage().instance().set(&StorageKey::TotalShares, &0i128); - env.storage().instance().set(&StorageKey::AccumulatedPremium, &0i128); - env.storage().instance().set(&StorageKey::Status, &PoolStatus::Active); - env.storage().instance().set(&StorageKey::LpList, &Vec::
::new(&env)); + env.storage().instance().set(&StorageKey::Initialized, &true); + env.storage().instance().set(&StorageKey::Admin, &admin); + env.storage().instance().set(&StorageKey::UsdcToken, &usdc_token); + env.storage().instance().set(&StorageKey::Treasury, &treasury); + env.storage().instance().set(&StorageKey::Backstop, &backstop); + env.storage().instance().set(&StorageKey::Category, &category); + env.storage().instance().set(&StorageKey::PolicyEngine, &policy_engine); + env.storage().instance().set(&StorageKey::ClaimsProcessor, &claims_processor); + env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); + env.storage().instance().set(&StorageKey::TotalLocked, &0i128); + env.storage().instance().set(&StorageKey::TotalShares, &0i128); + env.storage().instance().set(&StorageKey::AccumulatedPremium, &0i128); + env.storage().instance().set(&StorageKey::AccumulatedBackstop, &0i128); + env.storage().instance().set(&StorageKey::Status, &PoolStatus::Active); + env.storage().instance().set(&StorageKey::LpCount, &0u32); + // PendingAdmin is absent until propose_new_admin is called; no init needed. env.events().publish( (Symbol::new(&env, "initialized"),), Initialized { - admin: admin.clone(), - usdc_token: usdc_token.clone(), - treasury: treasury.clone(), - backstop: backstop.clone(), - category: category.clone(), + admin: admin.clone(), + usdc_token: usdc_token.clone(), + treasury: treasury.clone(), + backstop: backstop.clone(), + category: category.clone(), + policy_engine: policy_engine.clone(), + claims_processor: claims_processor.clone(), }, ); } @@ -195,10 +204,10 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); pos } None => { - let mut lp_list: Vec
= env.storage().instance() - .get(&StorageKey::LpList).unwrap_or_else(|| Vec::new(&env)); - lp_list.push_back(provider.clone()); - env.storage().instance().set(&StorageKey::LpList, &lp_list); + let count: u32 = env.storage().instance() + .get(&StorageKey::LpCount).unwrap_or(0); + env.storage().persistent().set(&StorageKey::LpAddress(count), &provider); + env.storage().instance().set(&StorageKey::LpCount, &(count + 1)); LpPosition { provider: provider.clone(), deposited: amount, @@ -229,6 +238,7 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); provider.require_auth(); // Guard: check for zero or negative shares input if shares <= 0 { panic_with_error!(&env, Error::ZeroAmount); } + Self::assert_active(&env); let lp_key = StorageKey::LpPosition(provider.clone()); let mut position: LpPosition = env.storage().persistent() @@ -293,6 +303,10 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); .get(&StorageKey::AccumulatedPremium).unwrap_or(0); env.storage().instance().set(&StorageKey::AccumulatedPremium, &(acc + lp_share)); + let acc_backstop: i128 = env.storage().instance() + .get(&StorageKey::AccumulatedBackstop).unwrap_or(0); + env.storage().instance().set(&StorageKey::AccumulatedBackstop, &(acc_backstop + backstop_share)); + env.events().publish( (Symbol::new(&env, "premium_distributed"),), PremiumDistributed { @@ -377,7 +391,7 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); // ── Capital locks ───────────────────────────────────────────────────────── pub fn lock_for_policy(env: Env, caller: Address, policy_id: u128, amount: i128) { - Self::require_admin(&env, &caller); + Self::require_protocol_caller(&env, &caller); // Guard: check for zero or negative lock amount input if amount <= 0 { panic_with_error!(&env, Error::ZeroAmount); } @@ -404,7 +418,7 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); } pub fn release_for_claim(env: Env, caller: Address, policy_id: u128) { - Self::require_admin(&env, &caller); + Self::require_protocol_caller(&env, &caller); let mut lock: CapitalLock = env.storage().persistent() .get(&StorageKey::Lock(policy_id)) .unwrap_or_else(|| panic_with_error!(&env, Error::LockNotFound)); @@ -428,7 +442,7 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); } pub fn release_for_expiry(env: Env, caller: Address, policy_id: u128) { - Self::require_admin(&env, &caller); + Self::require_protocol_caller(&env, &caller); let mut lock: CapitalLock = env.storage().persistent() .get(&StorageKey::Lock(policy_id)) .unwrap_or_else(|| panic_with_error!(&env, Error::LockNotFound)); @@ -443,12 +457,13 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); pub fn get_stats(env: Env) -> PoolStats { PoolStats { - category: env.storage().instance().get(&StorageKey::Category).unwrap(), - total_deposited: env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0), - total_locked: env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0), - total_shares: env.storage().instance().get(&StorageKey::TotalShares).unwrap_or(0), - accumulated_premium: env.storage().instance().get(&StorageKey::AccumulatedPremium).unwrap_or(0), - status: env.storage().instance().get(&StorageKey::Status).unwrap_or(PoolStatus::Active), + category: env.storage().instance().get(&StorageKey::Category).unwrap(), + total_deposited: env.storage().instance().get(&StorageKey::TotalDeposited).unwrap_or(0), + total_locked: env.storage().instance().get(&StorageKey::TotalLocked).unwrap_or(0), + total_shares: env.storage().instance().get(&StorageKey::TotalShares).unwrap_or(0), + accumulated_premium: env.storage().instance().get(&StorageKey::AccumulatedPremium).unwrap_or(0), + accumulated_backstop: env.storage().instance().get(&StorageKey::AccumulatedBackstop).unwrap_or(0), + status: env.storage().instance().get(&StorageKey::Status).unwrap_or(PoolStatus::Active), } } @@ -469,31 +484,28 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); } pub fn get_lp_count(env: Env) -> u32 { - let list: Vec
= env.storage().instance() - .get(&StorageKey::LpList) - .unwrap_or_else(|| Vec::new(&env)); - list.len() + env.storage().instance().get(&StorageKey::LpCount).unwrap_or(0) } pub fn get_lp_list(env: Env, offset: Option, limit: Option) -> PaginatedLps { - let list: Vec
= env.storage().instance() - .get(&StorageKey::LpList) - .unwrap_or_else(|| Vec::new(&env)); - let total_count = list.len(); - + let total_count: u32 = env.storage().instance() + .get(&StorageKey::LpCount).unwrap_or(0); + let offset_val = offset.unwrap_or(0); let limit_val = core::cmp::min(limit.unwrap_or(100), 500); - + let mut paginated = Vec::new(&env); if offset_val < total_count { let end = core::cmp::min(offset_val + limit_val, total_count); for i in offset_val..end { - if let Some(addr) = list.get(i) { + if let Some(addr) = env.storage().persistent() + .get::<_, Address>(&StorageKey::LpAddress(i)) + { paginated.push_back(addr); } } } - + PaginatedLps { lps: paginated, total_count, @@ -643,19 +655,16 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); pub fn accept_admin(env: Env, admin: Address) { let pending_admin: Address = env.storage().instance() .get(&StorageKey::PendingAdmin) - .unwrap_or_else(|| Address::from_uint(&env, 0)); + .unwrap_or_else(|| panic_with_error!(&env, Error::Unauthorized)); // Only the pending admin can accept if admin != pending_admin { panic_with_error!(&env, Error::Unauthorized); } admin.require_auth(); - let current_admin: Address = env.storage().instance() - .get(&StorageKey::Admin) - .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); // Update admin env.storage().instance().set(&StorageKey::Admin, &admin); // Clear the proposal - env.storage().instance().set(&StorageKey::PendingAdmin, &Address::from_uint(&env, 0)); + env.storage().instance().remove(&StorageKey::PendingAdmin); // Emit event env.events().publish( (Symbol::new(&env, "admin_updated"),), @@ -665,6 +674,21 @@ env.storage().instance().set(&StorageKey::TotalDeposited, &0i128); ); } + /// Enforces that only admin, the registered policy engine, or the registered + /// claims processor may call capital-lock functions. + fn require_protocol_caller(env: &Env, caller: &Address) { + let admin: Address = env.storage().instance().get(&StorageKey::Admin) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + let pe: Address = env.storage().instance().get(&StorageKey::PolicyEngine) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + let cp: Address = env.storage().instance().get(&StorageKey::ClaimsProcessor) + .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); + if *caller != admin && *caller != pe && *caller != cp { + panic_with_error!(env, Error::Unauthorized); + } + caller.require_auth(); + } + fn require_admin(env: &Env, caller: &Address) { let admin: Address = env.storage().instance().get(&StorageKey::Admin) .unwrap_or_else(|| panic_with_error!(env, Error::NotInitialized)); diff --git a/contracts/risk-pool/src/test.rs b/contracts/risk-pool/src/test.rs index 2ad09b9..43e4e48 100644 --- a/contracts/risk-pool/src/test.rs +++ b/contracts/risk-pool/src/test.rs @@ -16,14 +16,16 @@ fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Address) let env = Env::default(); env.mock_all_auths(); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); - let lp1 = Address::generate(&env); + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); - let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); let backstop_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); - let pool_id = env.register(RiskPool, ()); - let pool = RiskPoolClient::new(&env, &pool_id); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); let usdc_admin_client = token::StellarAssetClient::new(&env, &usdc_id); usdc_admin_client.mint(&lp1, &1_000_000_000_0000000i128); @@ -34,6 +36,8 @@ fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Address) &treasury, &backstop_id, &Symbol::new(&env, "crop"), + &policy_engine, + &claims_processor, ); (env, pool, usdc_id, admin, treasury, lp1) @@ -55,8 +59,10 @@ fn initialize_sets_state() { #[should_panic(expected = "Error(Contract, #1)")] fn cannot_initialize_twice() { let (env, pool, usdc, admin, treasury, _) = setup(); - let backstop = env.register_stellar_asset_contract_v2(admin.clone()).address(); - pool.initialize(&admin, &usdc, &treasury, &backstop, &Symbol::new(&env, "crop")); + let backstop = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); + pool.initialize(&admin, &usdc, &treasury, &backstop, &Symbol::new(&env, "crop"), &policy_engine, &claims_processor); } // ── deposits ────────────────────────────────────────────────────────────────── @@ -109,7 +115,7 @@ fn withdraw_full_position() { fn withdraw_uses_available_liquidity_after_locks() { let (_, pool, _, admin, _, lp1) = setup(); let amount = 1000_0000000i128; - let shares = pool.deposit(&lp1, &amount); + let shares = pool.deposit(&lp1, &amount, &0i128); pool.lock_for_policy(&admin, &1u128, &300_0000000i128); let returned = pool.withdraw(&lp1, &shares); @@ -121,7 +127,7 @@ fn withdraw_uses_available_liquidity_after_locks() { fn withdraw_uses_available_liquidity_after_locks_2() { let (_, pool, _, admin, _, lp1) = setup(); let amount = 1000_0000000i128; - let shares = pool.deposit(&lp1, &amount); + let shares = pool.deposit(&lp1, &amount, &0i128); pool.lock_for_policy(&admin, &1u128, &300_0000000i128); let returned = pool.withdraw(&lp1, &shares); @@ -243,7 +249,7 @@ fn double_lock_fails() { #[should_panic(expected = "Error(Contract, #10)")] fn double_release_fails() { let (_, pool, _, admin, _, lp1) = setup(); - pool.deposit(&lp1, &200_0000000i128); + pool.deposit(&lp1, &200_0000000i128, &0i128); pool.lock_for_policy(&admin, &99u128, &50_0000000i128); pool.release_for_claim(&admin, &99u128); pool.release_for_claim(&admin, &99u128); // already released @@ -255,7 +261,7 @@ fn double_release_fails() { #[test] fn lock_and_release_for_expiry_round_trip() { let (_, pool, _, admin, _, lp1) = setup(); - pool.deposit(&lp1, &200_0000000i128); + pool.deposit(&lp1, &200_0000000i128, &0i128); pool.lock_for_policy(&admin, &42u128, &100_0000000i128); assert_eq!(pool.get_utilization_rate(), 5_000u32); // 50% utilization in bps @@ -268,7 +274,7 @@ fn lock_and_release_for_expiry_round_trip() { #[test] fn lock_100_release_100_returns_total_locked_to_zero() { let (_, pool, _, admin, _, lp1) = setup(); - pool.deposit(&lp1, &200_0000000i128); + pool.deposit(&lp1, &200_0000000i128, &0i128); let lock_amount = 100_0000000i128; pool.lock_for_policy(&admin, &1u128, &lock_amount); @@ -283,7 +289,7 @@ fn lock_100_release_100_returns_total_locked_to_zero() { #[should_panic(expected = "Error(Contract, #10)")] fn double_release_for_expiry_fails() { let (_, pool, _, admin, _, lp1) = setup(); - pool.deposit(&lp1, &200_0000000i128); + pool.deposit(&lp1, &200_0000000i128, &0i128); pool.lock_for_policy(&admin, &99u128, &50_0000000i128); pool.release_for_expiry(&admin, &99u128); pool.release_for_expiry(&admin, &99u128); // already released @@ -296,7 +302,7 @@ fn double_release_for_expiry_fails() { fn pool_deposit_while_paused_fails() { let (_, pool, _, admin, _, lp1) = setup(); pool.pause(&admin); - pool.deposit(&lp1, &100_0000000i128); + pool.deposit(&lp1, &100_0000000i128, &0i128); } #[test] @@ -304,7 +310,7 @@ fn resume_allows_deposit() { let (_, pool, _, admin, _, lp1) = setup(); pool.pause(&admin); pool.resume(&admin); - let shares = pool.deposit(&lp1, &100_0000000i128); + let shares = pool.deposit(&lp1, &100_0000000i128, &0i128); assert!(shares > 0); } @@ -313,7 +319,7 @@ fn resume_allows_deposit() { #[test] fn get_position_returns_correct_state() { let (_, pool, _, _, _, lp1) = setup(); - pool.deposit(&lp1, &300_0000000i128); + pool.deposit(&lp1, &300_0000000i128, &0i128); let pos = pool.get_position(&lp1).unwrap(); assert_eq!(pos.deposited, 300_0000000i128); assert_eq!(pos.shares, 300_0000000i128 * 1_000_000_000); @@ -333,11 +339,11 @@ fn test_deposit_precision_loss_prevented() { let lp2 = Address::generate(&env); // LP1 deposits 1000 USDC - pool.deposit(&lp1, &1000_0000000i128); + pool.deposit(&lp1, &1000_0000000i128, &0i128); // LP2 deposits just 1 stroop (smallest possible unit) token::StellarAssetClient::new(&env, &usdc_id).mint(&lp2, &1i128); - let shares = pool.deposit(&lp2, &1i128); + let shares = pool.deposit(&lp2, &1i128, &0i128); // Because of the 1e9 precision multiplier, 1 stroop yields 1e9 shares. // This prevents truncation to 0 even if the pool's deposited amount grew @@ -360,7 +366,7 @@ fn test_deposit_precision_loss_prevented() { fn admin_cannot_drain_lp_funds_indirectly() { let (_, pool, _, admin, _, lp1) = setup(); let amount = 500_0000000i128; - let _shares = pool.deposit(&lp1, &amount); + let _shares = pool.deposit(&lp1, &amount, &0i128); // There is no admin-only withdraw function. Only `withdraw(lp, shares)` // exists, and it requires lp's auth. Verify there are no other @@ -378,10 +384,10 @@ fn admin_cannot_drain_lp_funds_indirectly() { /// Admin can request a withdrawal, but it cannot be executed before the /// 7-day timelock matures. #[test] -#[should_panic(expected = "Error(Contract, #14)")] +#[should_panic(expected = "Error(Contract, #15)")] fn admin_timelock_withdrawal_not_ready_before_7_days() { let (_, pool, _usdc_id, admin, _treasury, lp1) = setup(); - pool.deposit(&lp1, &1_000_0000000i128); + pool.deposit(&lp1, &1_000_0000000i128, &0i128); pool.request_admin_withdrawal(&admin, &100_0000000i128); // Attempt to execute immediately → TimelockNotReady (#14) @@ -392,7 +398,7 @@ fn admin_timelock_withdrawal_not_ready_before_7_days() { #[test] fn admin_timelock_cancel_and_re_request() { let (env, pool, _usdc_id, admin, _treasury, lp1) = setup(); - pool.deposit(&lp1, &1_000_0000000i128); + pool.deposit(&lp1, &1_000_0000000i128, &0i128); pool.request_admin_withdrawal(&admin, &100_0000000i128); pool.cancel_admin_withdrawal(&admin); @@ -413,7 +419,7 @@ fn admin_timelock_cancel_and_re_request() { #[should_panic(expected = "Error(Contract, #3)")] fn non_admin_cannot_lock_for_policy() { let (_, pool, _usdc_id, _admin, _treasury, lp1) = setup(); - pool.deposit(&lp1, &500_0000000i128); + pool.deposit(&lp1, &500_0000000i128, &0i128); pool.lock_for_policy(&lp1, &1u128, &100_0000000i128); } @@ -423,7 +429,7 @@ fn non_admin_cannot_lock_for_policy() { #[should_panic(expected = "Error(Contract, #3)")] fn non_admin_cannot_release_for_claim() { let (_, pool, _usdc_id, admin, _treasury, lp1) = setup(); - pool.deposit(&lp1, &500_0000000i128); + pool.deposit(&lp1, &500_0000000i128, &0i128); pool.lock_for_policy(&admin, &1u128, &100_0000000i128); pool.release_for_claim(&lp1, &1u128); @@ -434,20 +440,114 @@ fn non_admin_cannot_release_for_claim() { #[should_panic(expected = "Error(Contract, #3)")] fn non_admin_cannot_release_for_expiry() { let (_, pool, _usdc_id, admin, _treasury, lp1) = setup(); - pool.deposit(&lp1, &500_0000000i128); + pool.deposit(&lp1, &500_0000000i128, &0i128); pool.lock_for_policy(&admin, &1u128, &100_0000000i128); pool.release_for_expiry(&lp1, &1u128); +} + +// ── issue #84: policy engine / claims processor authorization ───────────────── + +/// Policy engine (registered at init) can lock capital. +#[test] +fn policy_engine_can_lock_for_policy() { + let (env, _pool, usdc_id, _admin, _treasury, lp1) = setup(); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); + let backstop = env.register_stellar_asset_contract_v2(_admin.clone()).address(); + + let pool2_id = env.register(RiskPool, ()); + let pool2 = RiskPoolClient::new(&env, &pool2_id); + token::StellarAssetClient::new(&env, &usdc_id).mint(&lp1, &1_000_0000000i128); + pool2.initialize( + &_admin, + &usdc_id, + &_treasury, + &backstop, + &Symbol::new(&env, "defi"), + &policy_engine, + &claims_processor, + ); + pool2.deposit(&lp1, &500_0000000i128, &0i128); + pool2.lock_for_policy(&policy_engine, &1u128, &100_0000000i128); + assert_eq!(pool2.get_utilization_rate(), 2_000u32); +} + +/// Claims processor (registered at init) can release capital. +#[test] +fn claims_processor_can_release_for_claim() { + let (env, _pool, usdc_id, _admin, _treasury, lp1) = setup(); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); + let backstop = env.register_stellar_asset_contract_v2(_admin.clone()).address(); + + let pool2_id = env.register(RiskPool, ()); + let pool2 = RiskPoolClient::new(&env, &pool2_id); + token::StellarAssetClient::new(&env, &usdc_id).mint(&lp1, &1_000_0000000i128); + pool2.initialize( + &_admin, + &usdc_id, + &_treasury, + &backstop, + &Symbol::new(&env, "defi"), + &policy_engine, + &claims_processor, + ); + pool2.deposit(&lp1, &500_0000000i128, &0i128); + pool2.lock_for_policy(&policy_engine, &1u128, &100_0000000i128); + pool2.release_for_claim(&claims_processor, &1u128); + assert_eq!(pool2.get_utilization_rate(), 0u32); +} + +/// Arbitrary address that is not admin, policy_engine, or claims_processor cannot lock. +#[test] +#[should_panic(expected = "Error(Contract, #3)")] +fn arbitrary_address_cannot_lock_for_policy() { + let (env, pool, _, _, _, lp1) = setup(); + pool.deposit(&lp1, &500_0000000i128, &0i128); + let attacker = Address::generate(&env); + pool.lock_for_policy(&attacker, &1u128, &100_0000000i128); +} + +// ── issue #85: backstop tracking in stats ───────────────────────────────────── + +/// receive_premium tracks backstop amount in get_stats. +#[test] +fn receive_premium_tracks_accumulated_backstop() { + let (_, pool, _, _, _, lp1) = setup(); + pool.deposit(&lp1, &1_000_0000000i128, &0i128); + pool.receive_premium(&lp1, &100_0000000i128); + let stats = pool.get_stats(); + // 10% of 100 USDC goes to backstop + assert_eq!(stats.accumulated_backstop, 10_0000000i128); + // 80% goes to LP accumulated + assert_eq!(stats.accumulated_premium, 80_0000000i128); +} + +// ── issue #86: withdraw respects PoolStatus::Paused ────────────────────────── + +/// Withdrawing while paused is rejected just like depositing while paused. +#[test] +#[should_panic(expected = "Error(Contract, #6)")] +fn pool_withdraw_while_paused_fails() { + let (_, pool, _, admin, _, lp1) = setup(); + let shares = pool.deposit(&lp1, &100_0000000i128, &0i128); + pool.pause(&admin); + pool.withdraw(&lp1, &shares); +} + +// ── issue #87: LP list pagination (now backed by persistent indexed storage) ── + #[test] fn test_get_lp_list_pagination() { - let (env, pool, usdc_id, _admin, _, lp1) = setup(); + let (env, pool, usdc_id, _admin, _, _lp1) = setup(); env.budget().reset_unlimited(); let usdc_client = token::StellarAssetClient::new(&env, &usdc_id); for _ in 0..200 { let lp = Address::generate(&env); usdc_client.mint(&lp, &10_000_000i128); - pool.deposit(&lp, &10_000_000i128); + pool.deposit(&lp, &10_000_000i128, &0i128); } assert_eq!(pool.get_lp_count(), 200); diff --git a/contracts/risk-pool/src/test_advanced.rs b/contracts/risk-pool/src/test_advanced.rs index 04b9e73..531d41a 100644 --- a/contracts/risk-pool/src/test_advanced.rs +++ b/contracts/risk-pool/src/test_advanced.rs @@ -15,15 +15,17 @@ fn setup_multi() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Ad let env = Env::default(); env.mock_all_auths(); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); - let lp1 = Address::generate(&env); - let lp2 = Address::generate(&env); + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + let lp2 = Address::generate(&env); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); let backstop_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); - let pool_id = env.register(RiskPool, ()); - let pool = RiskPoolClient::new(&env, &pool_id); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); let mint = |addr: &Address| { token::StellarAssetClient::new(&env, &usdc_id).mint(addr, &10_000_0000000i128); @@ -31,7 +33,15 @@ fn setup_multi() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Ad mint(&lp1); mint(&lp2); - pool.initialize(&admin, &usdc_id, &treasury, &backstop_id, &Symbol::new(&env, "defi")); + pool.initialize( + &admin, + &usdc_id, + &treasury, + &backstop_id, + &Symbol::new(&env, "defi"), + &policy_engine, + &claims_processor, + ); (env, pool, usdc_id, admin, treasury, lp1, lp2) } @@ -40,19 +50,19 @@ fn setup_multi() -> (Env, RiskPoolClient<'static>, Address, Address, Address, Ad fn lp_count_tracks_unique_depositors() { let (_, pool, _, _, _, lp1, lp2) = setup_multi(); assert_eq!(pool.get_lp_count(), 0); - pool.deposit(&lp1, &100_0000000i128); + pool.deposit(&lp1, &100_0000000i128, &0i128); assert_eq!(pool.get_lp_count(), 1); - pool.deposit(&lp2, &200_0000000i128); + pool.deposit(&lp2, &200_0000000i128, &0i128); assert_eq!(pool.get_lp_count(), 2); // Second deposit from lp1 should not increment count - pool.deposit(&lp1, &50_0000000i128); + pool.deposit(&lp1, &50_0000000i128, &0i128); assert_eq!(pool.get_lp_count(), 2); } #[test] fn available_liquidity_decreases_with_locks() { let (_, pool, _, admin, _, lp1, _) = setup_multi(); - pool.deposit(&lp1, &500_0000000i128); + pool.deposit(&lp1, &500_0000000i128, &0i128); assert_eq!(pool.get_available_liquidity(), 500_0000000i128); pool.lock_for_policy(&admin, &1u128, &200_0000000i128); assert_eq!(pool.get_available_liquidity(), 300_0000000i128); @@ -63,8 +73,8 @@ fn available_liquidity_decreases_with_locks() { #[test] fn two_lps_receive_proportional_yield() { let (_, pool, _, _, _, lp1, lp2) = setup_multi(); - pool.deposit(&lp1, &300_0000000i128); // 3/4 of pool - pool.deposit(&lp2, &100_0000000i128); // 1/4 of pool + pool.deposit(&lp1, &300_0000000i128, &0i128); // 3/4 of pool + pool.deposit(&lp2, &100_0000000i128, &0i128); // 1/4 of pool // premium: 400 USDC → 320 USDC to LP accumulated (80%) pool.receive_premium(&lp1, &400_0000000i128); @@ -79,8 +89,8 @@ fn two_lps_receive_proportional_yield() { #[test] fn get_stats_reflects_all_operations() { let (_, pool, _, admin, _, lp1, lp2) = setup_multi(); - pool.deposit(&lp1, &300_0000000i128); - pool.deposit(&lp2, &100_0000000i128); + pool.deposit(&lp1, &300_0000000i128, &0i128); + pool.deposit(&lp2, &100_0000000i128, &0i128); pool.lock_for_policy(&admin, &5u128, &80_0000000i128); pool.receive_premium(&lp1, &100_0000000i128); diff --git a/contracts/risk-pool/src/test_edge.rs b/contracts/risk-pool/src/test_edge.rs index 3021bcd..c2391fd 100644 --- a/contracts/risk-pool/src/test_edge.rs +++ b/contracts/risk-pool/src/test_edge.rs @@ -12,18 +12,28 @@ fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address) { let env = Env::default(); env.mock_all_auths(); - let admin = Address::generate(&env); - let treasury = Address::generate(&env); - let lp1 = Address::generate(&env); + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let lp1 = Address::generate(&env); + let policy_engine = Address::generate(&env); + let claims_processor = Address::generate(&env); let usdc_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); let backstop_id = env.register_stellar_asset_contract_v2(admin.clone()).address(); - let pool_id = env.register(RiskPool, ()); - let pool = RiskPoolClient::new(&env, &pool_id); + let pool_id = env.register(RiskPool, ()); + let pool = RiskPoolClient::new(&env, &pool_id); token::StellarAssetClient::new(&env, &usdc_id).mint(&lp1, &100_000_0000000i128); - pool.initialize(&admin, &usdc_id, &treasury, &backstop_id, &Symbol::new(&env, "crop")); + pool.initialize( + &admin, + &usdc_id, + &treasury, + &backstop_id, + &Symbol::new(&env, "crop"), + &policy_engine, + &claims_processor, + ); (env, pool, admin, treasury, lp1) } @@ -31,7 +41,7 @@ fn setup() -> (Env, RiskPoolClient<'static>, Address, Address, Address) { #[test] fn zero_accumulated_premium_yields_zero_claim() { let (_, pool, _, _, lp1) = setup(); - pool.deposit(&lp1, &100_0000000i128); + pool.deposit(&lp1, &100_0000000i128, &0i128); let yield_amount = pool.claim_yield(&lp1); assert_eq!(yield_amount, 0); } @@ -40,7 +50,7 @@ fn zero_accumulated_premium_yields_zero_claim() { fn full_deposit_withdraw_round_trip_no_premium() { let (_, pool, _, _, lp1) = setup(); let amount = 500_0000000i128; - let shares = pool.deposit(&lp1, &amount); + let shares = pool.deposit(&lp1, &amount, &0i128); let returned = pool.withdraw(&lp1, &shares); assert_eq!(returned, amount); let stats = pool.get_stats(); @@ -52,7 +62,7 @@ fn full_deposit_withdraw_round_trip_no_premium() { fn utilization_100_pct_after_locking_all() { let (_, pool, admin, _, lp1) = setup(); let amount = 200_0000000i128; - pool.deposit(&lp1, &amount); + pool.deposit(&lp1, &amount, &0i128); pool.lock_for_policy(&admin, &10u128, &amount); assert_eq!(pool.get_utilization_rate(), 10_000u32); // 100% in bps assert_eq!(pool.get_available_liquidity(), 0); @@ -61,7 +71,7 @@ fn utilization_100_pct_after_locking_all() { #[test] fn multiple_locks_and_releases_track_correctly() { let (_, pool, admin, _, lp1) = setup(); - pool.deposit(&lp1, &1000_0000000i128); + pool.deposit(&lp1, &1000_0000000i128, &0i128); pool.lock_for_policy(&admin, &1u128, &300_0000000i128); pool.lock_for_policy(&admin, &2u128, &200_0000000i128); assert_eq!(pool.get_stats().total_locked, 500_0000000i128); diff --git a/contracts/risk-pool/src/types.rs b/contracts/risk-pool/src/types.rs index 8ec7399..bc420d1 100644 --- a/contracts/risk-pool/src/types.rs +++ b/contracts/risk-pool/src/types.rs @@ -1,5 +1,13 @@ use soroban_sdk::{contracttype, Address, Symbol, Vec}; +/// Paginated result from `get_lp_list`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PaginatedLps { + pub lps: Vec
, + pub total_count: u32, +} + /// Status of a risk pool. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -40,12 +48,13 @@ pub struct CapitalLock { #[derive(Clone, Debug, Eq, PartialEq)] pub struct PoolStats { /// Category: "crop" | "flight" | "disaster" | "defi" - pub category: Symbol, - pub total_deposited: i128, - pub total_locked: i128, - pub total_shares: i128, - pub accumulated_premium: i128, - pub status: PoolStatus, + pub category: Symbol, + pub total_deposited: i128, + pub total_locked: i128, + pub total_shares: i128, + pub accumulated_premium: i128, + pub accumulated_backstop: i128, + pub status: PoolStatus, } // ─── Events ────────────────────────────────────────────────────────────────── @@ -53,11 +62,13 @@ pub struct PoolStats { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Initialized { - pub admin: Address, - pub usdc_token: Address, - pub treasury: Address, - pub backstop: Address, - pub category: Symbol, + pub admin: Address, + pub usdc_token: Address, + pub treasury: Address, + pub backstop: Address, + pub category: Symbol, + pub policy_engine: Address, + pub claims_processor: Address, } #[contracttype] @@ -161,6 +172,8 @@ pub struct AdminWithdrawalExecuted { #[derive(Clone, Debug, Eq, PartialEq)] pub struct AdminWithdrawalCancelled { pub admin: Address, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct AdminUpdated {