diff --git a/docs/stablecoin/README.md b/docs/stablecoin/README.md index fe3f1c1..cf9cf7b 100644 --- a/docs/stablecoin/README.md +++ b/docs/stablecoin/README.md @@ -594,7 +594,7 @@ These are properties the protocol maintains across every state-changing instruct | Constant / parameter | Bound | Rationale | |---|---|---| | `FIXED_POINT_ONE` | `10^27` | RAY precision; standard. | -| `stability_fee_per_millisecond` | `FIXED_POINT_ONE ≤ x ≤ FIXED_POINT_ONE * 2` | Lower bound = no decay (RFP "fees accrue continuously" implies positive rate). Upper bound is an anti-typo sanity cap (≈100%/ms) — it does **not** by itself prevent `compound_rate` overflow; that is handled by clamping the elapsed window (`MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS`, below). Real values are `1 + ε` where `ε ≈ 1.5×10^15` for ~5% annual. | +| `stability_fee_per_millisecond` | `FIXED_POINT_ONE ≤ x ≤ FIXED_POINT_ONE + FIXED_POINT_ONE / 100_000_000_000` | Lower bound = no decay (RFP "fees accrue continuously" implies positive rate). Upper bound caps the per-ms increment at `1e-11`, so `compound_rate(x, MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS)` stays inside `u128` under the one-day clamp. Real values are `1 + ε` where `ε ≈ 1.5×10^15` for ~5% annual. | | `minimum_collateralization_ratio` | `FIXED_POINT_ONE * 1.1 ≤ x ≤ FIXED_POINT_ONE * 10` | Lower bound = 110% (any less is liquidation-immediate); upper bound = 1000% (sanity cap). Real values are 130–200%. | | `controller_proportional_gain` magnitude | `|x| ≤ FIXED_POINT_ONE * 10^3` | Practical upper bound for rate-explosion guard (RFP R2). Real values are tiny (≈10^6–10^12 raw) because they scale price-error × per-ms rate-output. Rescaled `÷10^3` from the per-second formulation. | | `controller_integral_gain` magnitude | `|x| ≤ FIXED_POINT_ONE` | As proportional, but rescaled `÷10^6` (it also multiplies `Δt`, now in ms): real values ≈10^3–10^9 raw. | diff --git a/programs/integration_tests/tests/stablecoin.rs b/programs/integration_tests/tests/stablecoin.rs index bd20c9e..e9db204 100644 --- a/programs/integration_tests/tests/stablecoin.rs +++ b/programs/integration_tests/tests/stablecoin.rs @@ -24,6 +24,7 @@ use twap_oracle_core::OraclePriceAccount; const POSITION_NONCE: u64 = 7; const CLOCK_START: u64 = 1_000; const Q64_ONE: u128 = 18_446_744_073_709_551_616; +const TEST_STABILITY_FEE_RATE: u128 = FIXED_POINT_ONE + 1_000_000_000_000_000; struct Keys; struct Ids; @@ -498,7 +499,7 @@ fn fungible_supply(state: &V03State, account_id: AccountId) -> u128 { #[test] fn stablecoin_initialize_then_accrue_fee() { let mut state = state_for_stablecoin_tests(); - let rate = FIXED_POINT_ONE + FIXED_POINT_ONE / 10; + let rate = TEST_STABILITY_FEE_RATE; initialize_protocol(&mut state, rate); let params = protocol_parameters(&state); @@ -538,7 +539,7 @@ fn stablecoin_initialize_then_accrue_fee() { #[test] fn stablecoin_set_fee_rate_anchors_old_rate_before_switching() { let mut state = state_for_stablecoin_tests(); - let old_rate = FIXED_POINT_ONE + FIXED_POINT_ONE / 10; + let old_rate = TEST_STABILITY_FEE_RATE; initialize_protocol(&mut state, old_rate); advance_clock(&mut state, CLOCK_START + 2); diff --git a/programs/stablecoin/methods/guest/src/bin/stablecoin.rs b/programs/stablecoin/methods/guest/src/bin/stablecoin.rs index c8d29b1..1abd02c 100644 --- a/programs/stablecoin/methods/guest/src/bin/stablecoin.rs +++ b/programs/stablecoin/methods/guest/src/bin/stablecoin.rs @@ -115,6 +115,10 @@ mod stablecoin { } /// Open a new collateral-only position for the calling owner. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface passes explicit position, vault, token, and protocol accounts" + )] #[instruction] pub fn open_position( ctx: ProgramContext, @@ -230,6 +234,10 @@ mod stablecoin { } /// Repay stablecoin debt against an existing position. + #[expect( + clippy::too_many_arguments, + reason = "instruction interface passes explicit position, token, fee, and protocol accounts" + )] #[instruction] pub fn repay_debt( ctx: ProgramContext, diff --git a/programs/stablecoin/src/generate_debt.rs b/programs/stablecoin/src/generate_debt.rs index 0926cf8..5bac576 100644 --- a/programs/stablecoin/src/generate_debt.rs +++ b/programs/stablecoin/src/generate_debt.rs @@ -103,6 +103,11 @@ pub fn generate_debt( market_price_oracle.account_id, params.market_price_oracle_id, "Market price oracle does not match protocol parameters" ); + assert_ne!( + market_price_oracle.account, + Account::default(), + "Market price oracle account must be initialized" + ); let oracle = OraclePriceAccount::try_from(&market_price_oracle.account.data) .expect("Market price oracle account must hold a valid OraclePriceAccount"); assert_eq!( diff --git a/programs/stablecoin/src/tests.rs b/programs/stablecoin/src/tests.rs index fff2d05..ff075cd 100644 --- a/programs/stablecoin/src/tests.rs +++ b/programs/stablecoin/src/tests.rs @@ -18,7 +18,7 @@ use stablecoin_core::{ compute_stablecoin_definition_pda, compute_stablecoin_definition_pda_seed, compute_stablecoin_master_holding_pda, compute_stablecoin_master_holding_pda_seed, Position, ProtocolParameters, RedemptionPriceState, StabilityFeeAccumulator, FIXED_POINT_ONE, - MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS, + MAXIMUM_COMPOUNDING_WINDOW_MILLISECONDS, MAX_STABILITY_FEE_PER_MILLISECOND, }; use token_core::{TokenDefinition, TokenHolding}; use twap_oracle_core::OraclePriceAccount; @@ -788,6 +788,19 @@ fn set_stability_fee_rejects_rate_below_one() { ); } +#[test] +#[should_panic(expected = "Stability fee per millisecond is out of bounds")] +fn set_stability_fee_rejects_rate_above_safe_maximum() { + crate::set_stability_fee_per_millisecond::set_stability_fee_per_millisecond( + admin_account(), + protocol_parameters_account(false), + stability_fee_accumulator_account(FIXED_POINT_ONE, 1_000), + clock_account(1_000), + STABLECOIN_PROGRAM_ID, + MAX_STABILITY_FEE_PER_MILLISECOND + 1, + ); +} + #[test] fn open_position_stores_normalized_position_and_emits_token_calls() { let (post_states, chained_calls) = crate::open_position::open_position( @@ -961,6 +974,24 @@ fn generate_debt_rejects_wrong_stablecoin_definition() { ); } +#[test] +#[should_panic(expected = "Market price oracle account must be initialized")] +fn generate_debt_rejects_uninitialized_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), + uninit(oracle_id()), + 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;