mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 05:29:50 +00:00
fix(stablecoin): tighten fee bounds and validation
This commit is contained in:
parent
2df239e742
commit
785876370c
@ -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. |
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user