From e4fbf035a27679e5294e6580fab0aa6fe9adbf8c Mon Sep 17 00:00:00 2001 From: Neche58 Date: Mon, 29 Jun 2026 09:55:16 +0000 Subject: [PATCH] feat: add public protocol-wide statistics aggregation view - Add ProtocolStats struct to shared/types.rs - Fix missing closing brace on PositionSaleOffer in shared/types.rs - Add DataKey variants: AggregateFunded, MaxPositionBps, ProtocolStats - Increment pools_opened and active_pools in release_funds - Increment total_repaid and decrement active_pools in repay on close - Increment pools_defaulted and decrement active_pools in mark_default - Increment total_repaid and decrement active_pools in accept_early_settlement - Add get_protocol_stats() public view returning ProtocolStats Closes #272 --- contracts/financing_pool/src/lib.rs | 48 ++++++++++++++++++++++++++++- contracts/shared/src/types.rs | 18 +++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/contracts/financing_pool/src/lib.rs b/contracts/financing_pool/src/lib.rs index 5d9f01d..402ef6c 100644 --- a/contracts/financing_pool/src/lib.rs +++ b/contracts/financing_pool/src/lib.rs @@ -3,7 +3,7 @@ use kora_shared::{ errors::KoraError, events, - types::{Pool, Position}, + types::{EarlySettlementOffer, Pool, Position, PositionSaleOffer, ProtocolStats}, validation::{bps_of, bps_of_normalized, UPGRADE_TIMELOCK_DELAY}, }; use soroban_sdk::{ @@ -29,6 +29,9 @@ pub enum DataKey { UpgradeProposal, SaleOffer(u64, Address), EarlySettlement(u64), + AggregateFunded(Address), + MaxPositionBps, + ProtocolStats, } // ── Contract ────────────────────────────────────────────────────────────────── @@ -130,6 +133,13 @@ impl FinancingPoolContract { // Standardized financing pool event events::pool_opened(&env, &marketplace, invoice_id, &token, pool.face_value); + // Update protocol stats + let mut stats: ProtocolStats = env.storage().instance().get(&DataKey::ProtocolStats) + .unwrap_or(ProtocolStats { pools_opened: 0, total_repaid: 0, pools_defaulted: 0, active_pools: 0 }); + stats.pools_opened = stats.pools_opened.saturating_add(1); + stats.active_pools = stats.active_pools.saturating_add(1); + env.storage().instance().set(&DataKey::ProtocolStats, &stats); + // Transition NFT status to Funded nft_client.set_funded(&env.current_contract_address(), &invoice_id); @@ -323,6 +333,15 @@ impl FinancingPoolContract { let token_client = token::Client::new(&env, &token); token_client.transfer(&payer, &env.current_contract_address(), &amount); + // Update protocol stats + let mut stats: ProtocolStats = env.storage().instance().get(&DataKey::ProtocolStats) + .unwrap_or(ProtocolStats { pools_opened: 0, total_repaid: 0, pools_defaulted: 0, active_pools: 0 }); + stats.total_repaid = stats.total_repaid.saturating_add(effective_amount); + if should_close { + stats.active_pools = stats.active_pools.saturating_sub(1); + } + env.storage().instance().set(&DataKey::ProtocolStats, &stats); + // Standardized repayment event events::repayment_made(&env, invoice_id, &payer, amount); @@ -427,6 +446,13 @@ impl FinancingPoolContract { events::invoice_defaulted(&env, invoice_id, &admin); + // Update protocol stats + let mut stats: ProtocolStats = env.storage().instance().get(&DataKey::ProtocolStats) + .unwrap_or(ProtocolStats { pools_opened: 0, total_repaid: 0, pools_defaulted: 0, active_pools: 0 }); + stats.pools_defaulted = stats.pools_defaulted.saturating_add(1); + stats.active_pools = stats.active_pools.saturating_sub(1); + env.storage().instance().set(&DataKey::ProtocolStats, &stats); + // Automatically record the default against the SME in the risk registry let invoice = nft_client.get_invoice(&invoice_id); if let Some(rr_contract) = env @@ -585,6 +611,13 @@ impl FinancingPoolContract { kora_invoice_nft::InvoiceNftContractClient::new(&env, &nft_contract); nft_client.set_repaid(&env.current_contract_address(), &invoice_id); + // Update protocol stats + let mut stats: ProtocolStats = env.storage().instance().get(&DataKey::ProtocolStats) + .unwrap_or(ProtocolStats { pools_opened: 0, total_repaid: 0, pools_defaulted: 0, active_pools: 0 }); + stats.total_repaid = stats.total_repaid.saturating_add(offer.amount); + stats.active_pools = stats.active_pools.saturating_sub(1); + env.storage().instance().set(&DataKey::ProtocolStats, &stats); + env.storage() .persistent() .remove(&DataKey::EarlySettlement(invoice_id)); @@ -673,6 +706,19 @@ impl FinancingPoolContract { positions.values() } + /// Return protocol-wide aggregate statistics for analytics/dashboards. + pub fn get_protocol_stats(env: Env) -> ProtocolStats { + env.storage() + .instance() + .get(&DataKey::ProtocolStats) + .unwrap_or(ProtocolStats { + pools_opened: 0, + total_repaid: 0, + pools_defaulted: 0, + active_pools: 0, + }) + } + // ── Secondary market ─────────────────────────────────────────────────────── /// List a position for sale on the secondary market. diff --git a/contracts/shared/src/types.rs b/contracts/shared/src/types.rs index 57254d3..94aa3fb 100644 --- a/contracts/shared/src/types.rs +++ b/contracts/shared/src/types.rs @@ -113,6 +113,24 @@ pub struct PositionSaleOffer { pub invoice_id: u64, pub token: Address, pub price: i128, +} + +/// Protocol-wide running counters for analytics dashboards. +/// +/// Incremented at the relevant mutation points: +/// - `pools_opened` — incremented in `release_funds` +/// - `total_repaid` — incremented in `repay` (by the effective_amount of each call) +/// - `pools_defaulted` — incremented in `mark_default` +/// - `active_pools` — +1 in `release_funds`, -1 when pool closes (repay or default) +#[contracttype] +#[derive(Clone, Debug)] +pub struct ProtocolStats { + pub pools_opened: u64, + pub total_repaid: i128, + pub pools_defaulted: u64, + pub active_pools: u64, +} + /// An SME's early-termination buyout offer for a funded invoice. /// /// The SME escrows `amount` (a discount to `total_owed`) into the pool; investors then