use std::collections::HashMap;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::bond::BondOutcome;
use crate::error::{Result, ScemaDexError};
use crate::oracle::Reputation;
use crate::primitives::Usdc;
#[derive(Clone, Copy, Debug)]
pub struct PremiumConfig {
pub base_bps: u32,
pub min_factor_bps: u32,
pub confidence_k: u32,
}
impl Default for PremiumConfig {
fn default() -> Self {
Self {
base_bps: 500, min_factor_bps: 2_500, confidence_k: 10,
}
}
}
impl PremiumConfig {
pub fn premium(&self, coverage: Usdc, reputation: Reputation) -> Usdc {
let score = reputation.score.clamp(0.0, 1.0);
let samples = reputation.samples as f64;
let confidence = samples / (samples + self.confidence_k as f64); let earned_trust = score * confidence; let min_factor = self.min_factor_bps as f64 / 10_000.0;
let factor = min_factor + (1.0 - min_factor) * (1.0 - earned_trust); let premium = coverage.0 as f64 * (self.base_bps as f64 / 10_000.0) * factor;
Usdc(premium as u64)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InsuranceTerms {
pub coverage: Usdc,
pub premium: Usdc,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Policy {
pub intent_digest: String,
pub insured_id: String,
pub coverage: Usdc,
pub premium: Usdc,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Payout {
pub intent_digest: String,
pub insured_id: String,
pub outcome: BondOutcome,
pub paid: Usdc,
pub premium: Usdc,
pub shortfall: bool,
}
struct PoolState {
capital: u64,
policies: HashMap<String, Policy>,
}
pub struct InsurancePool {
config: PremiumConfig,
state: Mutex<PoolState>,
}
impl InsurancePool {
pub fn new(config: PremiumConfig) -> Self {
Self::with_capital(config, Usdc(0))
}
pub fn with_capital(config: PremiumConfig, seed: Usdc) -> Self {
Self {
config,
state: Mutex::new(PoolState {
capital: seed.0,
policies: HashMap::new(),
}),
}
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, PoolState>> {
self.state
.lock()
.map_err(|_| ScemaDexError::Bond("insurance pool lock poisoned".into()))
}
pub fn capital(&self) -> Usdc {
self.lock().map(|s| Usdc(s.capital)).unwrap_or(Usdc(0))
}
pub fn active_policies(&self) -> usize {
self.lock().map(|s| s.policies.len()).unwrap_or(0)
}
pub fn quote(&self, coverage: Usdc, reputation: Reputation) -> InsuranceTerms {
InsuranceTerms {
coverage,
premium: self.config.premium(coverage, reputation),
}
}
pub fn deposit(&self, amount: Usdc) -> Result<Usdc> {
let mut st = self.lock()?;
st.capital = st.capital.saturating_add(amount.0);
Ok(Usdc(st.capital))
}
pub fn bind(
&self,
intent_digest: impl Into<String>,
insured_id: impl Into<String>,
coverage: Usdc,
reputation: Reputation,
) -> Result<Policy> {
let premium = self.config.premium(coverage, reputation);
let mut st = self.lock()?;
if st.capital.saturating_add(premium.0) < coverage.0 {
return Err(ScemaDexError::Bond(
"insurance pool undercapitalized to bind this coverage".into(),
));
}
st.capital = st.capital.saturating_add(premium.0);
let policy = Policy {
intent_digest: intent_digest.into(),
insured_id: insured_id.into(),
coverage,
premium,
};
st.policies.insert(policy.intent_digest.clone(), policy.clone());
Ok(policy)
}
pub fn settle(&self, intent_digest: &str, outcome: BondOutcome) -> Result<Option<Payout>> {
let mut st = self.lock()?;
let policy = match st.policies.remove(intent_digest) {
Some(p) => p,
None => return Ok(None),
};
let (paid, shortfall) = match outcome {
BondOutcome::Honored => (0u64, false),
BondOutcome::Slashed => {
let pay = policy.coverage.0.min(st.capital);
st.capital -= pay;
(pay, pay < policy.coverage.0)
}
};
Ok(Some(Payout {
intent_digest: policy.intent_digest,
insured_id: policy.insured_id,
outcome,
paid: Usdc(paid),
premium: policy.premium,
shortfall,
}))
}
pub fn on_settlement(
&self,
intent_digest: &str,
outcome: BondOutcome,
insurance_slice: Usdc,
) -> Result<Option<Payout>> {
if outcome == BondOutcome::Slashed && insurance_slice.0 > 0 {
self.deposit(insurance_slice)?;
}
self.settle(intent_digest, outcome)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rep(score: f64, samples: u32) -> Reputation {
Reputation { score, samples }
}
#[test]
fn premium_falls_with_reputation_and_track_record() {
let cfg = PremiumConfig::default();
let cov = Usdc(1_000_000);
let unproven = cfg.premium(cov, rep(0.0, 0));
let trusted = cfg.premium(cov, rep(1.0, 1_000));
let trusted_but_new = cfg.premium(cov, rep(1.0, 1));
assert!(trusted < unproven, "a trusted, proven agent pays less");
assert!(
trusted_but_new > trusted,
"few samples earn little of the discount"
);
assert_eq!(unproven, Usdc(50_000));
}
#[test]
fn bind_requires_the_pool_to_back_the_coverage() {
let pool = InsurancePool::new(PremiumConfig::default());
assert!(pool.bind("d", "agent", Usdc(1_000_000), rep(0.5, 50)).is_err());
let pool = InsurancePool::with_capital(PremiumConfig::default(), Usdc(2_000_000));
let policy = pool.bind("d", "agent", Usdc(1_000_000), rep(0.5, 50)).unwrap();
assert_eq!(pool.capital(), Usdc(2_000_000 + policy.premium.0));
assert_eq!(pool.active_policies(), 1);
}
#[test]
fn honored_bond_keeps_the_premium_pays_nothing() {
let pool = InsurancePool::with_capital(PremiumConfig::default(), Usdc(2_000_000));
let policy = pool.bind("d", "agent", Usdc(1_000_000), rep(0.8, 100)).unwrap();
let before = pool.capital();
let payout = pool.settle("d", BondOutcome::Honored).unwrap().unwrap();
assert_eq!(payout.paid, Usdc(0));
assert_eq!(payout.premium, policy.premium);
assert_eq!(pool.capital(), before, "premium retained, nothing paid");
assert_eq!(pool.active_policies(), 0);
}
#[test]
fn slashed_bond_pays_coverage_from_the_pool() {
let pool = InsurancePool::with_capital(PremiumConfig::default(), Usdc(2_000_000));
pool.bind("d", "agent", Usdc(1_000_000), rep(0.8, 100)).unwrap();
let before = pool.capital();
let payout = pool.settle("d", BondOutcome::Slashed).unwrap().unwrap();
assert_eq!(payout.paid, Usdc(1_000_000));
assert!(!payout.shortfall);
assert_eq!(pool.capital(), Usdc(before.0 - 1_000_000));
}
#[test]
fn correlated_slashes_can_cap_a_payout() {
let cov = Usdc(1_000_000);
let pool = InsurancePool::with_capital(PremiumConfig::default(), cov);
pool.bind("a", "agent", cov, rep(0.5, 50)).unwrap();
pool.bind("b", "agent", cov, rep(0.5, 50)).unwrap();
let first = pool.settle("a", BondOutcome::Slashed).unwrap().unwrap();
assert_eq!(first.paid, cov);
assert!(!first.shortfall);
let second = pool.settle("b", BondOutcome::Slashed).unwrap().unwrap();
assert!(second.paid < cov, "only the remaining capital is paid");
assert!(second.shortfall);
}
#[test]
fn on_settlement_banks_the_slash_slice_before_paying() {
let pool = InsurancePool::with_capital(PremiumConfig::default(), Usdc(1_000_000));
pool.bind("d", "agent", Usdc(1_000_000), rep(0.9, 200)).unwrap();
let cap_after_bind = pool.capital();
let payout = pool
.on_settlement("d", BondOutcome::Slashed, Usdc(300_000))
.unwrap()
.unwrap();
assert_eq!(payout.paid, Usdc(1_000_000));
assert_eq!(pool.capital(), Usdc(cap_after_bind.0 + 300_000 - 1_000_000));
}
#[test]
fn settle_without_a_policy_is_none() {
let pool = InsurancePool::new(PremiumConfig::default());
assert!(pool.settle("missing", BondOutcome::Slashed).unwrap().is_none());
}
}