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 {