diff --git a/contracts/insurance_pool/src/lib.rs b/contracts/insurance_pool/src/lib.rs index 5ce35b61..33459336 100644 --- a/contracts/insurance_pool/src/lib.rs +++ b/contracts/insurance_pool/src/lib.rs @@ -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. @@ -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] @@ -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 { + 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() @@ -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::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. diff --git a/contracts/insurance_pool/src/test.rs b/contracts/insurance_pool/src/test.rs index c64e608f..9324c89d 100644 --- a/contracts/insurance_pool/src/test.rs +++ b/contracts/insurance_pool/src/test.rs @@ -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); +} + diff --git a/docs/error-codes.md b/docs/error-codes.md index 5301b393..90c326ab 100644 --- a/docs/error-codes.md +++ b/docs/error-codes.md @@ -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`. | ---