Skip to content
Merged
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
45 changes: 43 additions & 2 deletions contracts/insurance_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ pub enum InsuranceError {
NoPendingProposal = 6,
/// The proposal's timelock has not yet expired.
TimelockNotExpired = 7,
/// A checked arithmetic operation overflowed.
ArithmeticOverflow = 8,
/// Premium deposit would exceed the configured pool balance cap.
BalanceCapExceeded = 9,
}

/// Storage keys for the pool.
Expand Down Expand Up @@ -91,6 +95,8 @@ pub enum DataKey {
ClaimCount(Address),
/// Tiered coverage caps: (tier_threshold, coverage_amount) (Issue #528).
CoverageTiers,
/// Optional maximum pool balance cap (governance-configurable).
BalanceCap,
}

#[contract]
Expand Down Expand Up @@ -488,6 +494,27 @@ impl InsurancePool {
Ok(())
}

/// Get the current pool balance cap, or `None` if uncapped.
pub fn get_balance_cap(env: Env) -> Option<i128> {
env.storage().instance().get(&DataKey::BalanceCap)
}

/// Set (or clear) the pool balance cap. Pass `0` to remove the cap.
/// Requires admin auth.
pub fn set_balance_cap(env: Env, cap: i128) -> Result<(), InsuranceError> {
Self::require_admin(&env);
if cap < 0 {
return Err(InsuranceError::InvalidAmount);
}
if cap == 0 {
env.storage().instance().remove(&DataKey::BalanceCap);
} else {
env.storage().instance().set(&DataKey::BalanceCap, &cap);
}
env.events().publish((symbol_short!("cap_set"),), cap);
Ok(())
}

fn require_admin(env: &Env) -> Address {
match env
.storage()
Expand Down Expand Up @@ -545,15 +572,29 @@ impl InsurancePoolInterface for InsurancePool {
.persistent()
.get(&DataKey::Premiums(lp.clone()))
.unwrap_or(0);
let new_premium = prev_premium
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(&env, InsuranceError::ArithmeticOverflow));
env.storage().persistent().set(
&DataKey::Premiums(lp.clone()),
&prev_premium.saturating_add(amount),
&new_premium,
);

let balance: i128 = env.storage().instance().get(&DataKey::Balance).unwrap_or(0);
let new_balance = balance
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(&env, InsuranceError::ArithmeticOverflow));

// Enforce the optional balance cap.
if let Some(cap) = env.storage().instance().get::<DataKey, i128>(&DataKey::BalanceCap) {
if new_balance > cap {
panic_with_error!(&env, InsuranceError::BalanceCapExceeded);
}
}

env.storage()
.instance()
.set(&DataKey::Balance, &balance.saturating_add(amount));
.set(&DataKey::Balance, &new_balance);

// Transfer tokens from LP to pool (checks-effects-interactions pattern).
// State changes above must complete before this external call.
Expand Down
100 changes: 90 additions & 10 deletions contracts/insurance_pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,17 +408,97 @@ fn coverage_update_affects_future_claims() {
let s = setup();
let lp = Address::generate(&s.env);

// Deposit with default coverage
s.client.deposit_premium(&lp, &(COVERAGE * 2));
let payout1 = s.client.claim(&1);
assert_eq!(payout1, COVERAGE);

// Update coverage to higher value
// Deposit with default coverage (LP pays > 50% → tier 4: 150%)
let deposit_large = COVERAGE * 2;
s.token_admin.mint(&lp, &deposit_large);
s.client.deposit_premium(&lp, &deposit_large);
let payout1 = s.client.claim(&1, &lp);
assert_eq!(payout1, (COVERAGE * 150) / 100);

// Update coverage to higher value via governance
let new_coverage = 3_000_000_000;
s.client.set_coverage_via_governance(&new_coverage);

// Reset balance for testing
s.client.deposit_premium(&lp, &(new_coverage * 2));
let payout2 = s.client.claim(&2);
assert_eq!(payout2, new_coverage);
// Add more balance so the pool can pay out at the new coverage level
let deposit2 = new_coverage * 2;
s.token_admin.mint(&lp, &deposit2);
s.client.deposit_premium(&lp, &deposit2);
let payout2 = s.client.claim(&2, &lp);
// LP still has > 50% of new coverage → tier 4 (150%)
assert_eq!(payout2, (new_coverage * 150) / 100);
}

// ── Overflow and balance cap tests ──────────────────────────────────────────

#[test]
fn deposit_premium_at_i128_max_overflows() {
let s = setup();

// Seed the premium record to (i128::MAX - 1) by depositing a large amount.
// Then depositing 1 more triggers a checked overflow.
let near_max: i128 = i128::MAX - 1;
// Mint enough tokens; i128::MAX won't fit in a typical test but we can simulate
// the state by pre-setting storage and then trying to add 2.
// We use two deposits: first near_max, then a small extra amount.
s.token_admin.mint(&s.lp, &near_max);
// This should succeed (within i128 bounds)
s.client.deposit_premium(&s.lp, &near_max);
assert_eq!(s.client.get_premiums_paid(&s.lp), near_max);

// Now depositing even 1 more should overflow the pool balance (near_max + 1 > i128::MAX - 1, balance check).
// Actually balance is now near_max; adding 1 more gives i128::MAX which barely fits.
// Adding 2 will cause i128 checked_add to return None.
s.token_admin.mint(&s.lp, &2);
let result = s.client.try_deposit_premium(&s.lp, &2);
// checked_add(near_max + 2) overflows → ArithmeticOverflow
assert!(
result.is_err(),
"Depositing beyond i128::MAX must fail with overflow error"
);
}

#[test]
fn deposit_premium_enforces_balance_cap() {
let s = setup();

// Set a cap of 1_000 tokens
let cap: i128 = 1_000;
s.client.set_balance_cap(&cap);
assert_eq!(s.client.get_balance_cap(), Some(cap));

// Deposit up to the cap — should succeed
s.token_admin.mint(&s.lp, &cap);
s.client.deposit_premium(&s.lp, &cap);
assert_eq!(s.client.get_pool_balance(), cap);

// Depositing even 1 more must be rejected
s.token_admin.mint(&s.lp, &1);
let result = s.client.try_deposit_premium(&s.lp, &1);
assert!(
result.is_err(),
"Deposit exceeding the balance cap must fail"
);

// Pool balance must remain unchanged after the failed deposit
assert_eq!(s.client.get_pool_balance(), cap);
}

#[test]
fn balance_cap_can_be_cleared() {
let s = setup();

let cap: i128 = 500;
s.client.set_balance_cap(&cap);
assert_eq!(s.client.get_balance_cap(), Some(cap));

// Clear cap by passing 0
s.client.set_balance_cap(&0);
assert_eq!(s.client.get_balance_cap(), None);

// Now deposits beyond the old cap should succeed
let amount: i128 = 1_000;
s.token_admin.mint(&s.lp, &amount);
s.client.deposit_premium(&s.lp, &amount);
assert_eq!(s.client.get_pool_balance(), amount);
}

2 changes: 2 additions & 0 deletions docs/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Source of truth: [`contracts/insurance_pool/src/lib.rs`](../contracts/insurance_
| 5 | `AlreadyInitialized` | Contract is already initialised. | `initialize()` called more than once. | Initialisation is one-shot; redeploy if a fresh pool is needed. |
| 6 | `NoPendingProposal` | No pending proposal exists for the requested admin action. | `execute_coverage_change` / `execute_admin_transfer` / `cancel_*` called with no queued proposal. | Queue a proposal first via `propose_coverage_change` or `propose_admin_transfer`. |
| 7 | `TimelockNotExpired` | The proposal's timelock has not yet expired. | Attempted to execute a timelocked action before the 3-day delay elapsed. | Wait until `env.ledger().timestamp() >= eta` and retry. |
| 8 | `ArithmeticOverflow` | A checked arithmetic operation overflowed during premium accumulation. | Depositing an amount that would overflow `i128` on the running balance or per-LP premium counter. | Ensure deposit amounts are within sane bounds; this should only occur with extreme/malicious inputs. |
| 9 | `BalanceCapExceeded` | Premium deposit would push the pool balance above the configured cap. | The admin has set a `BalanceCap` and the incoming deposit would exceed it. | Reduce the deposit amount or ask the admin to raise the cap via `set_balance_cap`. |

---

Expand Down
Loading