Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,36 @@ Only approved applications create grants. Governance confirms milestones in
order with `confirm_milestone`; the recipient pulls confirmed funds with
`claim`.

## 9. Backstop Per-Feed Underwriting Exposure & Concentration Caps (`RefractBackstop`)

[`backstop/src/lib.rs`](./backstop/src/lib.rs) enforces per-feed and per-policyholder
concentration limits so that no single oracle feed or correlated risk bucket can
monopolize the backstop's underwriting capacity:

* **Per-Feed & Per-Holder Exposure Ledger:**
* `feed_exposure(feed)` tracks active in-force coverage underwritten on a given
feed symbol (`depeg`, `crash`, `liq`, `sc_risk`, `flight`, or custom feeds).
* `holder_feed_exposure(holder, feed)` tracks active coverage held by a single
policyholder on `feed`.
* `aggregate_feed_exposure()` maintains the sum of active exposures across all
tracked feeds ($\sum_{f \in \text{TrackedFeeds}} \text{feed\_exposure}(f) = \text{aggregate\_feed\_exposure}()$).
* **Configurable Concentration Caps (`set_feed_cap`):**
* `max_feed_coverage`: absolute maximum active coverage on `feed` (`0` = unlimited).
* `max_feed_coverage_bps`: relative maximum active coverage on `feed` in basis
points of `total_capital` (`0..=10_000`, `0` = unlimited).
* `max_holder_feed_coverage`: maximum active coverage a single policyholder may
hold on `feed` (`0` = unlimited).
* Lowering a cap below current active exposure does **not** invalidate existing
in-force policies; it only blocks new policy sales on that feed until active
exposure decays below the updated cap.
* **Policy Lifecycle Enforcement & Idempotent Release:**
* `check_exposure_caps` and `record_policy_exposure` reject policy sales that
would exceed `max_feed_coverage` or `max_feed_coverage_bps` with
`BackstopError::FeedCapExceeded`, or `max_holder_feed_coverage` with
`BackstopError::HolderFeedCapExceeded`.
* `release_policy_exposure(caller, policy_id)` is idempotent per `policy_id`:
once a policy's `PolicyExposureRecord` transitions `active: true -> false`
upon claim settlement or expiry, any subsequent release call for the same
`policy_id` is a no-op returning `Ok(0)` and never double-decrements exposure.


287 changes: 286 additions & 1 deletion backstop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@ enum DataKey {
NextLotId,
NextLossId,
UnabsorbedShortfall,
FeedCap(Symbol),
FeedExposure(Symbol),
HolderFeedExposure(Address, Symbol),
PolicyExposure(u64),
AggregateFeedExposure,
TrackedFeeds,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FeedCapConfig {
/// Maximum absolute active coverage allowed on this feed (`0` = unlimited).
pub max_feed_coverage: i128,
/// Maximum active coverage on this feed in basis points of `total_capital` (`0` = unlimited).
pub max_feed_coverage_bps: i128,
/// Maximum active coverage a single policyholder may hold on this feed (`0` = unlimited).
pub max_holder_feed_coverage: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyExposureRecord {
pub policy_id: u64,
pub holder: Address,
pub feed: Symbol,
pub amount: i128,
pub active: bool,
}

#[contracttype]
Expand Down Expand Up @@ -69,6 +96,10 @@ pub enum BackstopError {
PendingLossFreeze = 10,
InvalidPremiumSplit = 11,
DuplicateLossId = 12,
InvalidFeedCap = 13,
FeedCapExceeded = 14,
HolderFeedCapExceeded = 15,
DuplicatePolicyExposure = 16,
}

#[contracttype]
Expand Down Expand Up @@ -131,13 +162,15 @@ impl RefractBackstop {
premium_income: 0,
},
);
env.storage().instance().set(&DataKey::LockupLedgers, &100u32);
env.storage().instance().set(&DataKey::LockupLedgers, &0u32);
env.storage().instance().set(&DataKey::JuniorPremiumBps, &(BPS_DENOM / 2));
env.storage().instance().set(&DataKey::PendingLoss, &false);
env.storage().instance().set(&DataKey::NextLotId, &1u64);
env.storage().instance().set(&DataKey::AllLots, &Vec::<u64>::new(&env));
env.storage().instance().set(&DataKey::NextLossId, &1u64);
env.storage().instance().set(&DataKey::UnabsorbedShortfall, &0i128);
env.storage().instance().set(&DataKey::AggregateFeedExposure, &0i128);
env.storage().instance().set(&DataKey::TrackedFeeds, &Vec::<Symbol>::new(&env));
for tranche in [Tranche::Junior, Tranche::Senior] {
env.storage().instance().set(
&DataKey::TrancheState(tranche),
Expand Down Expand Up @@ -696,6 +729,258 @@ impl RefractBackstop {
env.storage().instance().set(&"config", &config);
Ok(())
}

// ─── Issue #240: Per-Feed Underwriting Exposure Limits & Concentration Caps ──

/// Configure absolute, relative (bps of `total_capital`), and per-holder
/// underwriting exposure caps for `feed`. Admin-gated.
///
/// Lowering a cap below the feed's current active exposure is permitted and
/// does not invalidate existing in-force policies; it only blocks new policy
/// sales on that feed until exposure decays below the new cap.
pub fn set_feed_cap(
env: Env,
caller: Address,
feed: Symbol,
max_feed_coverage: i128,
max_feed_coverage_bps: i128,
max_holder_feed_coverage: i128,
) -> Result<(), BackstopError> {
caller.require_auth();
let config = Self::get_config(&env)?;
if caller != config.admin {
return Err(BackstopError::Unauthorized);
}
if max_feed_coverage < 0
|| !(0..=BPS_DENOM).contains(&max_feed_coverage_bps)
|| max_holder_feed_coverage < 0
{
return Err(BackstopError::InvalidFeedCap);
}
let cap = FeedCapConfig {
max_feed_coverage,
max_feed_coverage_bps,
max_holder_feed_coverage,
};
env.storage()
.persistent()
.set(&DataKey::FeedCap(feed.clone()), &cap);
env.events().publish(
(symbol_short!("FEEDCAP"), feed),
(
max_feed_coverage,
max_feed_coverage_bps,
max_holder_feed_coverage,
),
);
Ok(())
}

/// Return the configured exposure caps for `feed`, if any.
pub fn feed_cap(env: Env, feed: Symbol) -> Option<FeedCapConfig> {
env.storage().persistent().get(&DataKey::FeedCap(feed))
}

/// Return the active underwritten coverage currently tracked on `feed`.
pub fn feed_exposure(env: Env, feed: Symbol) -> i128 {
env.storage()
.persistent()
.get(&DataKey::FeedExposure(feed))
.unwrap_or(0)
}

/// Return the active underwritten coverage held by `holder` on `feed`.
pub fn holder_feed_exposure(env: Env, holder: Address, feed: Symbol) -> i128 {
env.storage()
.persistent()
.get(&DataKey::HolderFeedExposure(holder, feed))
.unwrap_or(0)
}

/// Return the aggregate active underwritten coverage across all tracked feeds.
pub fn aggregate_feed_exposure(env: Env) -> i128 {
env.storage()
.instance()
.get(&DataKey::AggregateFeedExposure)
.unwrap_or(0)
}

/// Return the list of feeds that have had underwriting exposure recorded.
pub fn tracked_feeds(env: Env) -> Vec<Symbol> {
env.storage()
.instance()
.get(&DataKey::TrackedFeeds)
.unwrap_or(Vec::new(&env))
}

/// Return the recorded exposure metadata for `policy_id`, if any.
pub fn policy_exposure(env: Env, policy_id: u64) -> Option<PolicyExposureRecord> {
env.storage()
.persistent()
.get(&DataKey::PolicyExposure(policy_id))
}

/// Pure read-only check verifying whether adding `coverage_amount` for
/// `(holder, feed)` stays within the feed's absolute cap, relative bps cap,
/// and per-holder-per-feed cap.
pub fn check_exposure_caps(
env: Env,
holder: Address,
feed: Symbol,
coverage_amount: i128,
) -> Result<(), BackstopError> {
if coverage_amount <= 0 {
return Err(BackstopError::InvalidAmount);
}
let state = Self::get_state(&env)?;
let Some(cap) = Self::feed_cap(env.clone(), feed.clone()) else {
return Ok(());
};
let next_feed = Self::feed_exposure(env.clone(), feed.clone()) + coverage_amount;
if cap.max_feed_coverage > 0 && next_feed > cap.max_feed_coverage {
return Err(BackstopError::FeedCapExceeded);
}
if cap.max_feed_coverage_bps > 0 {
let max_by_bps = state.total_capital * cap.max_feed_coverage_bps / BPS_DENOM;
if next_feed > max_by_bps {
return Err(BackstopError::FeedCapExceeded);
}
}
if cap.max_holder_feed_coverage > 0 {
let next_holder =
Self::holder_feed_exposure(env, holder, feed) + coverage_amount;
if next_holder > cap.max_holder_feed_coverage {
return Err(BackstopError::HolderFeedCapExceeded);
}
}
Ok(())
}

/// Record underwriting exposure for a newly issued policy after enforcing
/// all configured concentration caps. Callable only by the pool or admin.
pub fn record_policy_exposure(
env: Env,
caller: Address,
policy_id: u64,
holder: Address,
feed: Symbol,
coverage_amount: i128,
) -> Result<(), BackstopError> {
caller.require_auth();
let config = Self::get_config(&env)?;
if caller != config.pool && caller != config.admin {
return Err(BackstopError::Unauthorized);
}
if env
.storage()
.persistent()
.has(&DataKey::PolicyExposure(policy_id))
{
return Err(BackstopError::DuplicatePolicyExposure);
}
Self::check_exposure_caps(env.clone(), holder.clone(), feed.clone(), coverage_amount)?;

let prev_feed = Self::feed_exposure(env.clone(), feed.clone());
env.storage()
.persistent()
.set(&DataKey::FeedExposure(feed.clone()), &(prev_feed + coverage_amount));

let prev_holder = Self::holder_feed_exposure(env.clone(), holder.clone(), feed.clone());
env.storage().persistent().set(
&DataKey::HolderFeedExposure(holder.clone(), feed.clone()),
&(prev_holder + coverage_amount),
);

let prev_agg = Self::aggregate_feed_exposure(env.clone());
env.storage()
.instance()
.set(&DataKey::AggregateFeedExposure, &(prev_agg + coverage_amount));

let mut feeds = Self::tracked_feeds(env.clone());
let mut seen = false;
for f in feeds.iter() {
if f == feed {
seen = true;
break;
}
}
if !seen {
feeds.push_back(feed.clone());
env.storage().instance().set(&DataKey::TrackedFeeds, &feeds);
}

let record = PolicyExposureRecord {
policy_id,
holder,
feed: feed.clone(),
amount: coverage_amount,
active: true,
};
env.storage()
.persistent()
.set(&DataKey::PolicyExposure(policy_id), &record);

env.events().publish(
(symbol_short!("EXP_ADD"), feed),
(policy_id, coverage_amount),
);
Ok(())
}

/// Release underwriting exposure when a policy settles or expires.
/// Idempotent per `policy_id`: a subsequent call for an already-released
/// policy (e.g., if both settlement and expiry paths invoke release) is a
/// safe no-op returning `Ok(0)` and never double-decrements exposure.
pub fn release_policy_exposure(
env: Env,
caller: Address,
policy_id: u64,
) -> Result<i128, BackstopError> {
caller.require_auth();
let config = Self::get_config(&env)?;
if caller != config.pool && caller != config.admin {
return Err(BackstopError::Unauthorized);
}
let Some(mut record) = env
.storage()
.persistent()
.get::<DataKey, PolicyExposureRecord>(&DataKey::PolicyExposure(policy_id))
else {
return Ok(0);
};
if !record.active {
return Ok(0);
}
record.active = false;
env.storage()
.persistent()
.set(&DataKey::PolicyExposure(policy_id), &record);

let prev_feed = Self::feed_exposure(env.clone(), record.feed.clone());
env.storage().persistent().set(
&DataKey::FeedExposure(record.feed.clone()),
&(prev_feed - record.amount).max(0),
);

let prev_holder =
Self::holder_feed_exposure(env.clone(), record.holder.clone(), record.feed.clone());
env.storage().persistent().set(
&DataKey::HolderFeedExposure(record.holder, record.feed.clone()),
&(prev_holder - record.amount).max(0),
);

let prev_agg = Self::aggregate_feed_exposure(env.clone());
env.storage().instance().set(
&DataKey::AggregateFeedExposure,
&(prev_agg - record.amount).max(0),
);

env.events().publish(
(symbol_short!("EXP_REL"), record.feed),
(policy_id, record.amount),
);
Ok(record.amount)
}
}

#[cfg(test)]
Expand Down
Loading