-
Notifications
You must be signed in to change notification settings - Fork 3
feat: PERC-8461 — enable recurring maintenance fees per spec §8.2 #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,16 @@ pub const MAX_POSITION_ABS: u128 = 100_000_000_000_000_000_000; | |
| /// realistic deployment while still fitting comfortably within u128 arithmetic. | ||
| pub const MAX_VAULT_TVL: u128 = 1_000_000_000_000_000_000_000_000_000_000; // 10^30 | ||
|
|
||
| /// Maximum allowed maintenance_fee_per_slot in Params (spec §8.2). | ||
| /// 10^12 per slot ≈ 1 USDC/slot at 6-decimal precision. | ||
| /// Validated in Params::validate() — admin cannot set higher. | ||
| pub const MAX_MAINTENANCE_FEE_PER_SLOT: u128 = 1_000_000_000_000; | ||
|
|
||
| /// Maximum fee that a single settle_maintenance_fee_internal call may charge (spec §8.2). | ||
| /// Prevents a stale account from accumulating unbounded fee debt in one settlement. | ||
| /// 10^18 ≈ 1 billion USDC at 6 decimals — generous cap that still prevents u128 overflow. | ||
| pub const MAX_PROTOCOL_FEE_ABS: u128 = 1_000_000_000_000_000_000; | ||
|
|
||
| // ============================================================================ | ||
| // BPF-Safe 128-bit Types (see src/i128.rs) | ||
| // ============================================================================ | ||
|
|
@@ -595,6 +605,11 @@ impl RiskParams { | |
| // Insurance floor (spec §1.4: 0 <= I_floor <= MAX_ORACLE_PRICE * MAX_POSITION_ABS) | ||
| // No separate MAX_VAULT_TVL constant in this fork; nonzero values are permissive. | ||
| // Structural validity is enforced: insurance_floor must fit within U128. | ||
|
|
||
| // Maintenance fee per slot must not exceed cap (spec §8.2) | ||
| if self.maintenance_fee_per_slot.get() > MAX_MAINTENANCE_FEE_PER_SLOT { | ||
| return Err(RiskError::Overflow); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -1944,10 +1959,73 @@ impl RiskEngine { | |
| } | ||
| } | ||
|
|
||
| /// settle_maintenance_fee_internal (spec §8.2): update last_fee_slot for account. | ||
| #[allow(dead_code)] | ||
| /// settle_maintenance_fee_internal (spec §8.2): compute and charge recurring maintenance fees. | ||
| /// | ||
| /// Algorithm (upstream e357d431): | ||
| /// 1. Compute dt = now_slot - last_fee_slot (checked to prevent underflow) | ||
| /// 2. If dt == 0, no-op | ||
| /// 3. fee_due = maintenance_fee_per_slot * dt (checked_mul prevents overflow) | ||
| /// 4. Validate fee_due <= MAX_PROTOCOL_FEE_ABS (rejects absurd accumulations) | ||
| /// 5. Stamp last_fee_slot BEFORE charging (prevents re-charge on retry/failure) | ||
| /// 6. Deduct from fee_credits; if negative, pay from capital into insurance | ||
| /// | ||
| /// Used by force_close_resolved and keeper_crank paths where margin checks | ||
| /// are inappropriate (the caller handles position closure/liquidation). | ||
| fn settle_maintenance_fee_internal(&mut self, idx: usize, now_slot: u64) -> Result<()> { | ||
| let fee_per_slot = self.params.maintenance_fee_per_slot.get(); | ||
| if fee_per_slot == 0 { | ||
| // No maintenance fee configured — just advance the slot marker | ||
| self.accounts[idx].last_fee_slot = now_slot; | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let last_slot = self.accounts[idx].last_fee_slot; | ||
| // Checked subtraction: now_slot must be >= last_fee_slot | ||
| let dt = now_slot.checked_sub(last_slot).ok_or(RiskError::Overflow)?; | ||
| if dt == 0 { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| // Compute fee_due with checked arithmetic (prevents u128 overflow) | ||
| let fee_due = fee_per_slot | ||
| .checked_mul(dt as u128) | ||
| .ok_or(RiskError::Overflow)?; | ||
|
|
||
| // Cap: reject absurd fee accumulation (e.g. stale account with huge dt) | ||
| if fee_due > MAX_PROTOCOL_FEE_ABS { | ||
| return Err(RiskError::Overflow); | ||
| } | ||
|
|
||
| // CRITICAL: stamp last_fee_slot BEFORE charging. | ||
| // If the charge below fails or the transaction is retried, we will NOT | ||
| // re-compute the same fee window. This is the upstream CEI pattern. | ||
| self.accounts[idx].last_fee_slot = now_slot; | ||
|
|
||
| // Deduct from fee_credits (coupon buffer) | ||
| let fc = self.accounts[idx].fee_credits.get(); | ||
| let new_fc = fc.checked_sub(fee_due as i128).ok_or(RiskError::Overflow)?; | ||
| self.accounts[idx].fee_credits = I128::new(new_fc); | ||
|
|
||
| // If fee_credits went negative, pay debt from capital into insurance | ||
| if new_fc < 0 { | ||
| let debt = neg_i128_to_u128(new_fc); | ||
| let cap = self.accounts[idx].capital.get(); | ||
| let pay = core::cmp::min(debt, cap); | ||
| if pay > 0 { | ||
| self.set_capital(idx, cap - pay); | ||
| let pay_i128 = core::cmp::min(pay, i128::MAX as u128) as i128; | ||
| self.accounts[idx].fee_credits = I128::new( | ||
| self.accounts[idx] | ||
| .fee_credits | ||
| .get() | ||
| .checked_add(pay_i128) | ||
| .ok_or(RiskError::Overflow)?, | ||
| ); | ||
| self.insurance_fund.balance += pay; | ||
| self.insurance_fund.fee_revenue += pay; | ||
| } | ||
| } | ||
|
Comment on lines
+1962
to
+2027
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unify §8.2 fee calculation across all settlement entrypoints. This helper now enforces checked arithmetic and 🤖 Prompt for AI Agents |
||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7013,3 +7013,170 @@ fn test_deposit_ghost_account_no_state_leak_on_cap_failure() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "capital must not change on failed deposit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // PERC-8461: Recurring Maintenance Fees (spec §8.2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================== | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Helper: create params with a nonzero maintenance_fee_per_slot. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn params_with_maintenance_fee(fee_per_slot: u128) -> RiskParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut p = default_params(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p.maintenance_fee_per_slot = U128::new(fee_per_slot); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Basic fee accrual: after N slots, fee_due = fee_per_slot * N is charged. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_basic_accrual() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let fee_per_slot = 10u128; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(fee_per_slot))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let idx = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(idx, 100_000, 0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 1_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.recompute_aggregates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_conserved(&engine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Advance 100 slots via the public settle_maintenance_fee path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let dt = 100u64; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let now_slot = dt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let paid = engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .settle_maintenance_fee(idx, now_slot, DEFAULT_ORACLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // fee_due = 10 * 100 = 1000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // fee_credits starts at 0, goes to -1000, then capital pays 1000 into insurance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(paid, fee_per_slot * dt as u128); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].last_fee_slot, now_slot); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Capital reduced by 1000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].capital.get(), 100_000 - 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_conserved(&engine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Zero dt is a no-op — no fee charged. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_zero_dt_noop() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(10))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let idx = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(idx, 100_000, 0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 1_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Set last_fee_slot = 50, then call with now_slot = 50 → dt = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.accounts[idx as usize].last_fee_slot = 50; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let paid = engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .settle_maintenance_fee(idx, 50, DEFAULT_ORACLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(paid, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].capital.get(), 100_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fee charges through force_close_resolved path (internal fn). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_via_force_close_resolved() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let fee_per_slot = 5u128; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(fee_per_slot))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let user = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(user, 10_000, 0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.recompute_aggregates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_conserved(&engine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Advance current_slot so force_close_resolved will compute dt > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.current_slot = 200; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // last_fee_slot defaults to current_slot at deposit time (0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // So dt = 200, fee_due = 5 * 200 = 1000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let capital_returned = engine.force_close_resolved(user).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Capital was 10_000, fee charged = 1000, returned = 10_000 - 1000 = 9_000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(capital_returned, 9_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(!engine.is_used(user as usize)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Validate Params rejects maintenance_fee_per_slot > MAX_MAINTENANCE_FEE_PER_SLOT. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_params_validation() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use percolator::MAX_MAINTENANCE_FEE_PER_SLOT; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // At the limit — should be accepted | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut p = params_with_maintenance_fee(MAX_MAINTENANCE_FEE_PER_SLOT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(p.validate().is_ok(), "fee at cap must be accepted"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Above the limit — rejected | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p.maintenance_fee_per_slot = U128::new(MAX_MAINTENANCE_FEE_PER_SLOT + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(p.validate().is_err(), "fee above cap must be rejected"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fee with zero fee_per_slot is no-op even with large dt. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_zero_rate_noop() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(0))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let idx = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(idx, 100_000, 0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 1_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let paid = engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .settle_maintenance_fee(idx, 1_000_000, DEFAULT_ORACLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(paid, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].capital.get(), 100_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fee deducted from fee_credits first, then capital if credits go negative. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_credits_buffer() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let fee_per_slot = 10u128; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(fee_per_slot))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let idx = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(idx, 100_000, 0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 1_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Give account 500 fee credits (coupon) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.accounts[idx as usize].fee_credits = I128::new(500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.recompute_aggregates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_conserved(&engine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 100 slots → fee_due = 1000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // fee_credits: 500 - 1000 = -500 → pay 500 from capital | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let paid = engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .settle_maintenance_fee(idx, 100, DEFAULT_ORACLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(paid, 500); // only the capital portion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].capital.get(), 100_000 - 500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Fee with insufficient capital: pays what it can, remainder stays as debt. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_partial_payment() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let fee_per_slot = 100u128; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut engine = Box::new(RiskEngine::new(params_with_maintenance_fee(fee_per_slot))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let idx = engine.add_user(0).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.deposit(idx, 500, 0).unwrap(); // small capital | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set_insurance(&mut engine, 1_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine.recompute_aggregates(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_conserved(&engine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 100 slots → fee_due = 10_000, but capital is only 500 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let paid = engine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .settle_maintenance_fee(idx, 100, DEFAULT_ORACLE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(paid, 500); // all capital consumed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(engine.accounts[idx as usize].capital.get(), 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remaining debt stays in fee_credits (negative) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(engine.accounts[idx as usize].fee_credits.get() < 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// MAX_PROTOCOL_FEE_ABS and MAX_MAINTENANCE_FEE_PER_SLOT constants are sane. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fn test_maintenance_fee_constants() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use percolator::{MAX_MAINTENANCE_FEE_PER_SLOT, MAX_PROTOCOL_FEE_ABS}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(MAX_MAINTENANCE_FEE_PER_SLOT, 1_000_000_000_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(MAX_PROTOCOL_FEE_ABS, 1_000_000_000_000_000_000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MAX_MAINTENANCE_FEE_PER_SLOT * u16::MAX should not exceed MAX_PROTOCOL_FEE_ABS | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (i.e., even at max rate for max funding dt, the fee is within cap) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let max_fee = MAX_MAINTENANCE_FEE_PER_SLOT * (u16::MAX as u128); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_fee <= MAX_PROTOCOL_FEE_ABS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "max_fee_per_slot * max_dt must fit within MAX_PROTOCOL_FEE_ABS" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+7167
to
+7181
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assertion is too weak: maintenance fee Stronger cap-bound assertion and test intent // MAX_MAINTENANCE_FEE_PER_SLOT * u16::MAX should not exceed MAX_PROTOCOL_FEE_ABS
-// (i.e., even at max rate for max funding dt, the fee is within cap)
-let max_fee = MAX_MAINTENANCE_FEE_PER_SLOT * (u16::MAX as u128);
+// Maintenance dt is slot-based (u64). Bound should be derived from the cap itself.
+let dt_cap = MAX_PROTOCOL_FEE_ABS / MAX_MAINTENANCE_FEE_PER_SLOT;
+let max_fee = MAX_MAINTENANCE_FEE_PER_SLOT * dt_cap;
assert!(
max_fee <= MAX_PROTOCOL_FEE_ABS,
- "max_fee_per_slot * max_dt must fit within MAX_PROTOCOL_FEE_ABS"
+ "max_fee_per_slot * dt_cap must fit within MAX_PROTOCOL_FEE_ABS"
);
+assert!(
+ MAX_MAINTENANCE_FEE_PER_SLOT.saturating_mul(dt_cap + 1) > MAX_PROTOCOL_FEE_ABS,
+ "dt above cap-derived bound must exceed MAX_PROTOCOL_FEE_ABS"
+);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.