mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-07-03 21:49:28 +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 |
|
| Constant / parameter | Bound | Rationale |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `FIXED_POINT_ONE` | `10^27` | RAY precision; standard. |
|
| `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%. |
|
| `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_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. |
|
| `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 POSITION_NONCE: u64 = 7;
|
||||||
const CLOCK_START: u64 = 1_000;
|
const CLOCK_START: u64 = 1_000;
|
||||||
const Q64_ONE: u128 = 18_446_744_073_709_551_616;
|
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 Keys;
|
||||||
struct Ids;
|
struct Ids;
|
||||||
@ -498,7 +499,7 @@ fn fungible_supply(state: &V03State, account_id: AccountId) -> u128 {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stablecoin_initialize_then_accrue_fee() {
|
fn stablecoin_initialize_then_accrue_fee() {
|
||||||
let mut state = state_for_stablecoin_tests();
|
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);
|
initialize_protocol(&mut state, rate);
|
||||||
|
|
||||||
let params = protocol_parameters(&state);
|
let params = protocol_parameters(&state);
|
||||||
@ -538,7 +539,7 @@ fn stablecoin_initialize_then_accrue_fee() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stablecoin_set_fee_rate_anchors_old_rate_before_switching() {
|
fn stablecoin_set_fee_rate_anchors_old_rate_before_switching() {
|
||||||
let mut state = state_for_stablecoin_tests();
|
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);
|
initialize_protocol(&mut state, old_rate);
|
||||||
|
|
||||||
advance_clock(&mut state, CLOCK_START + 2);
|
advance_clock(&mut state, CLOCK_START + 2);
|
||||||
|
|||||||
@ -115,6 +115,10 @@ mod stablecoin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open a new collateral-only position for the calling owner.
|
/// 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]
|
#[instruction]
|
||||||
pub fn open_position(
|
pub fn open_position(
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
@ -230,6 +234,10 @@ mod stablecoin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Repay stablecoin debt against an existing position.
|
/// Repay stablecoin debt against an existing position.
|
||||||
|
#[expect(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "instruction interface passes explicit position, token, fee, and protocol accounts"
|
||||||
|
)]
|
||||||
#[instruction]
|
#[instruction]
|
||||||
pub fn repay_debt(
|
pub fn repay_debt(
|
||||||
ctx: ProgramContext,
|
ctx: ProgramContext,
|
||||||
|
|||||||
@ -103,6 +103,11 @@ pub fn generate_debt(
|
|||||||
market_price_oracle.account_id, params.market_price_oracle_id,
|
market_price_oracle.account_id, params.market_price_oracle_id,
|
||||||
"Market price oracle does not match protocol parameters"
|
"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)
|
let oracle = OraclePriceAccount::try_from(&market_price_oracle.account.data)
|
||||||
.expect("Market price oracle account must hold a valid OraclePriceAccount");
|
.expect("Market price oracle account must hold a valid OraclePriceAccount");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@ -18,7 +18,7 @@ use stablecoin_core::{
|
|||||||
compute_stablecoin_definition_pda, compute_stablecoin_definition_pda_seed,
|
compute_stablecoin_definition_pda, compute_stablecoin_definition_pda_seed,
|
||||||
compute_stablecoin_master_holding_pda, compute_stablecoin_master_holding_pda_seed, Position,
|
compute_stablecoin_master_holding_pda, compute_stablecoin_master_holding_pda_seed, Position,
|
||||||
ProtocolParameters, RedemptionPriceState, StabilityFeeAccumulator, FIXED_POINT_ONE,
|
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 token_core::{TokenDefinition, TokenHolding};
|
||||||
use twap_oracle_core::OraclePriceAccount;
|
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]
|
#[test]
|
||||||
fn open_position_stores_normalized_position_and_emits_token_calls() {
|
fn open_position_stores_normalized_position_and_emits_token_calls() {
|
||||||
let (post_states, chained_calls) = crate::open_position::open_position(
|
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]
|
#[test]
|
||||||
fn repay_debt_uses_floor_rounding_against_current_accumulator() {
|
fn repay_debt_uses_floor_rounding_against_current_accumulator() {
|
||||||
let accumulator = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;
|
let accumulator = FIXED_POINT_ONE + FIXED_POINT_ONE / 10;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user