From e4aa722d6689b091ae18dd744698b92b9bfc087c Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Mon, 29 Jun 2026 15:03:56 -0300 Subject: [PATCH] fix(stablecoin): harden stability fee edge cases --- programs/stablecoin/src/generate_debt.rs | 5 ++++- programs/stablecoin/src/open_position.rs | 4 ++-- programs/stablecoin/src/shared.rs | 12 ++---------- programs/stablecoin/src/tests.rs | 20 +++++++++++++++++++- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/programs/stablecoin/src/generate_debt.rs b/programs/stablecoin/src/generate_debt.rs index f798984..7c2348c 100644 --- a/programs/stablecoin/src/generate_debt.rs +++ b/programs/stablecoin/src/generate_debt.rs @@ -123,8 +123,11 @@ pub fn generate_debt( oracle.price != 0, "Market price oracle price must be non-zero" ); + let oracle_age = now + .checked_sub(oracle.timestamp) + .expect("Market price oracle timestamp is in the future"); assert!( - now.saturating_sub(oracle.timestamp) <= params.maximum_oracle_price_age_milliseconds, + oracle_age <= params.maximum_oracle_price_age_milliseconds, "Market price oracle is stale" ); diff --git a/programs/stablecoin/src/open_position.rs b/programs/stablecoin/src/open_position.rs index 4d0eafd..924f19e 100644 --- a/programs/stablecoin/src/open_position.rs +++ b/programs/stablecoin/src/open_position.rs @@ -22,8 +22,8 @@ use crate::shared::{read_clock_timestamp, read_protocol_parameters}; /// - `position` or `vault` is already initialized. /// - `position.account_id` / `vault.account_id` do not match their PDA derivations. /// - `user_holding` cannot be decoded as a [`TokenHolding`]. -/// - `user_holding`'s definition does not match `token_definition`. -/// - `token_definition.program_owner` does not match `user_holding.program_owner`. +/// - `user_holding`'s definition does not match `collateral_definition`. +/// - `collateral_definition.program_owner` does not match `user_holding.program_owner`. #[expect( clippy::too_many_arguments, reason = "instruction surface passes explicit owner, position, vault, collateral, and protocol accounts" diff --git a/programs/stablecoin/src/shared.rs b/programs/stablecoin/src/shared.rs index ea4a828..28bf4e2 100644 --- a/programs/stablecoin/src/shared.rs +++ b/programs/stablecoin/src/shared.rs @@ -7,7 +7,7 @@ use nssa_core::{ use stablecoin_core::{ compute_protocol_parameters_pda, compute_redemption_price_state_pda, compute_stability_fee_accumulator_pda, current_accumulated_rate, ProtocolParameters, - RedemptionPriceState, StabilityFeeAccumulator, MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS, + RedemptionPriceState, StabilityFeeAccumulator, }; pub(crate) fn read_clock_timestamp(clock: &AccountWithMetadata) -> u64 { @@ -84,17 +84,9 @@ pub(crate) fn accrue_stability_fee_state( params: &ProtocolParameters, now: u64, ) -> StabilityFeeAccumulator { - let elapsed = now - .saturating_sub(accumulator.last_accrued_at) - .min(MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS); - let last_accrued_at = accumulator - .last_accrued_at - .checked_add(elapsed) - .expect("Clamped elapsed timestamp cannot overflow"); - StabilityFeeAccumulator { accumulated_rate_at_last_accrual: current_accumulated_rate(accumulator, params, now), - last_accrued_at, + last_accrued_at: now, } } diff --git a/programs/stablecoin/src/tests.rs b/programs/stablecoin/src/tests.rs index ecea7b4..f004270 100644 --- a/programs/stablecoin/src/tests.rs +++ b/programs/stablecoin/src/tests.rs @@ -685,7 +685,7 @@ fn accrue_stability_fee_clamps_elapsed_window() { ); assert_eq!( updated.last_accrued_at, - MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS + MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS + 1 ); } @@ -992,6 +992,24 @@ fn generate_debt_rejects_uninitialized_market_price_oracle() { ); } +#[test] +#[should_panic(expected = "Market price oracle timestamp is in the future")] +fn generate_debt_rejects_future_market_price_oracle() { + crate::generate_debt::generate_debt( + owner_account(), + position_account(1_000, 0), + stablecoin_definition_account(0), + user_stablecoin_holding(0), + stability_fee_accumulator_account(FIXED_POINT_ONE, 1_000), + redemption_price_state_account(FIXED_POINT_ONE, 1_000), + oracle_account(1_001), + protocol_parameters_account(false), + clock_account(1_000), + STABLECOIN_PROGRAM_ID, + 100, + ); +} + #[test] fn repay_debt_uses_floor_rounding_against_current_accumulator() { let accumulator = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;