levana_perpswap_cosmos/contracts/market/
config.rs

1//! Market-wide configuration
2
3pub mod defaults;
4use crate::prelude::*;
5
6use self::defaults::ConfigDefaults;
7
8use super::spot_price::{SpotPriceConfig, SpotPriceConfigInit};
9
10/// Configuration info for the vAMM
11/// Set by admin-only
12
13/// Since this tends to cross the message boundary
14/// all the numeric types are u32 or lower
15/// helper functions are available where more bits are needed
16#[cw_serde]
17pub struct Config {
18    /// The fee to open a position, as a percentage of the notional size
19    pub trading_fee_notional_size: Decimal256,
20    /// The fee to open a position, as a percentage of the counter-side collateral
21    pub trading_fee_counter_collateral: Decimal256,
22    /// default number of crank exeuctions to do when none specified
23    pub crank_execs: u32,
24    /// The maximum allowed leverage when opening a position
25    pub max_leverage: Number,
26    /// Impacts how much the funding rate changes in response to net notional changes.
27    pub funding_rate_sensitivity: Decimal256,
28    /// The maximum annualized rate for a funding payment
29    pub funding_rate_max_annualized: Decimal256,
30    /// The minimum annualized rate for borrow fee payments
31    pub borrow_fee_rate_min_annualized: NumberGtZero,
32    /// The maximum annualized rate for borrow fee payments
33    pub borrow_fee_rate_max_annualized: NumberGtZero,
34    /// Needed to ensure financial model is balanced
35    ///
36    /// Must be at most 1 less than the [Config::max_leverage]
37    pub carry_leverage: Decimal256,
38    /// Do not emit events (default is false, events *will* be emitted)
39    pub mute_events: bool,
40    /// Delay between liquifundings, in seconds
41    pub liquifunding_delay_seconds: u32,
42    /// The percentage of fees that are taken for the protocol
43    pub protocol_tax: Decimal256,
44    /// How long it takes to unstake xLP tokens into LP tokens, in seconds
45    pub unstake_period_seconds: u32,
46    /// Target utilization ratio liquidity, given as a ratio. (Must be between 0 and 1).
47    pub target_utilization: NonZero<Decimal256>,
48    /// Borrow fee sensitivity parameter.
49    ///
50    /// See [section 5.5 of the whitepaper](https://www.notion.so/levana-protocol/Levana-Well-funded-Perpetuals-Whitepaper-9805a6eba56d429b839f5551dbb65c40#295f9f2689e74ccab16ca28177eb32cb).
51    pub borrow_fee_sensitivity: NumberGtZero,
52    /// Maximum multiplier for xLP versus LP borrow fee shares.
53    ///
54    /// For example, if this number is 5, then as liquidity in the protocol
55    /// approaches 100% in LP and 0% in xLP, any xLP token will receive 5x the
56    /// rewards of an LP token.
57    pub max_xlp_rewards_multiplier: NumberGtZero,
58    /// Minimum counterpoint to [Config::max_xlp_rewards_multiplier]
59    pub min_xlp_rewards_multiplier: NumberGtZero,
60    /// Delta neutrality fee sensitivity parameter.
61    ///
62    /// Higher values indicate markets with greater depth of liquidity, and allow for
63    /// larger divergence for delta neutrality in the markets.
64    ///
65    /// This value is specified in the notional asset.
66    pub delta_neutrality_fee_sensitivity: NumberGtZero,
67    /// Delta neutrality fee cap parameter, given as a percentage
68    pub delta_neutrality_fee_cap: NumberGtZero,
69    /// Proportion of delta neutrality inflows that are sent to the protocol.
70    pub delta_neutrality_fee_tax: Decimal256,
71    /// The crank fee to be paid into the system, in collateral
72    pub crank_fee_charged: Usd,
73    /// The crank surcharge charged for every 10 items in the deferred execution queue.
74    ///
75    /// This is intended to create backpressure in times of high congestion.
76    ///
77    /// For every 10 items in the deferred execution queue, this amount is added to the
78    /// crank fee charged on performing a deferred execution message.
79    ///
80    /// This is only charged while adding new items to the queue, not when performing
81    /// ongoing tasks like liquifunding or liquidations.
82    #[serde(default = "ConfigDefaults::crank_fee_surcharge")]
83    pub crank_fee_surcharge: Usd,
84    /// The crank fee to be sent to crankers, in collateral
85    pub crank_fee_reward: Usd,
86    /// Minimum deposit collateral, given in USD
87    pub minimum_deposit_usd: Usd,
88    /// The liquifunding delay fuzz factor, in seconds.
89    ///
90    /// Up to how many seconds will we perform a liquifunding early. This will
91    /// be part of a semi-randomly generated value and will allow us to schedule
92    /// liquifundings arbitrarily to smooth out spikes in traffic.
93    #[serde(default = "ConfigDefaults::liquifunding_delay_fuzz_seconds")]
94    pub liquifunding_delay_fuzz_seconds: u32,
95    /// The maximum amount of liquidity that can be deposited into the market.
96    #[serde(default)]
97    pub max_liquidity: MaxLiquidity,
98    /// Disable the ability to proxy CW721 execution messages for positions.
99    /// Even if this is true, queries will still work as usual.
100    #[serde(default)]
101    pub disable_position_nft_exec: bool,
102    /// The liquidity cooldown period.
103    ///
104    /// After depositing new funds into the market, liquidity providers will
105    /// have a period of time where they cannot withdraw their funds. This is
106    /// intended to prevent an MEV attack where someone can reorder transactions
107    /// to extract fees from traders without taking on any impairment risk.
108    ///
109    /// This protection is only triggered by deposit of new funds; reinvesting
110    /// existing yield does not introduce a cooldown.
111    ///
112    /// While the cooldown is in place, providers are prevented from either
113    /// withdrawing liquidity or transferring their LP and xLP tokens.
114    ///
115    /// For migration purposes, this value defaults to 0, meaning no cooldown period.
116    #[serde(default)]
117    pub liquidity_cooldown_seconds: u32,
118
119    /// Ratio of notional size used for the exposure component of the liquidation margin.
120    #[serde(default = "ConfigDefaults::exposure_margin_ratio")]
121    pub exposure_margin_ratio: Decimal256,
122
123    /// Portion of trading fees given as rewards to referrers.
124    #[serde(default = "ConfigDefaults::referral_reward_ratio")]
125    pub referral_reward_ratio: Decimal256,
126
127    /// The spot price config for this market
128    pub spot_price: SpotPriceConfig,
129
130    // Fields below here are no longer used by the protocol, but kept in the data structure to ease migration.
131    /// Just for historical reasons/migrations
132    #[serde(rename = "price_update_too_old_seconds")]
133    pub _unused1: Option<u32>,
134    /// Just for historical reasons/migrations
135    #[serde(rename = "unpend_limit")]
136    pub _unused2: Option<u32>,
137    /// Just for historical reasons/migrations
138    #[serde(rename = "limit_order_fee")]
139    pub _unused3: Option<Collateral>,
140    /// Just for historical reasons/migrations
141    #[serde(rename = "staleness_seconds")]
142    pub _unused4: Option<u32>,
143}
144
145/// Maximum liquidity for deposit.
146///
147/// Note that this limit can be exceeded due to changes in collateral asset
148/// price or impairment.
149#[cw_serde]
150pub enum MaxLiquidity {
151    /// No bounds on how much liquidity can be deposited.
152    Unlimited {},
153    /// Only allow the given amount in USD.
154    ///
155    /// The exchange rate at time of deposit will be used.
156    Usd {
157        /// Amount in USD
158        amount: NonZero<Usd>,
159    },
160}
161
162impl Default for MaxLiquidity {
163    fn default() -> Self {
164        MaxLiquidity::Unlimited {}
165    }
166}
167
168impl Config {
169    /// create a new config with default values and a given spot price config
170    pub fn new(spot_price: SpotPriceConfig) -> Self {
171        // these unwraps are fine since we define the value
172        Self {
173            trading_fee_notional_size: ConfigDefaults::trading_fee_notional_size(),
174            trading_fee_counter_collateral: ConfigDefaults::trading_fee_counter_collateral(),
175            crank_execs: ConfigDefaults::crank_execs(),
176            max_leverage: ConfigDefaults::max_leverage(),
177            carry_leverage: ConfigDefaults::carry_leverage(),
178            funding_rate_max_annualized: ConfigDefaults::funding_rate_max_annualized(),
179            borrow_fee_rate_min_annualized: ConfigDefaults::borrow_fee_rate_min_annualized(),
180            borrow_fee_rate_max_annualized: ConfigDefaults::borrow_fee_rate_max_annualized(),
181            funding_rate_sensitivity: ConfigDefaults::funding_rate_sensitivity(),
182            mute_events: ConfigDefaults::mute_events(),
183            liquifunding_delay_seconds: ConfigDefaults::liquifunding_delay_seconds(),
184            protocol_tax: ConfigDefaults::protocol_tax(),
185            unstake_period_seconds: ConfigDefaults::unstake_period_seconds(),
186            target_utilization: ConfigDefaults::target_utilization(),
187            borrow_fee_sensitivity: ConfigDefaults::borrow_fee_sensitivity(),
188            max_xlp_rewards_multiplier: ConfigDefaults::max_xlp_rewards_multiplier(),
189            min_xlp_rewards_multiplier: ConfigDefaults::min_xlp_rewards_multiplier(),
190            delta_neutrality_fee_sensitivity: ConfigDefaults::delta_neutrality_fee_sensitivity(),
191            delta_neutrality_fee_cap: ConfigDefaults::delta_neutrality_fee_cap(),
192            delta_neutrality_fee_tax: ConfigDefaults::delta_neutrality_fee_tax(),
193            crank_fee_charged: ConfigDefaults::crank_fee_charged(),
194            crank_fee_surcharge: ConfigDefaults::crank_fee_surcharge(),
195            crank_fee_reward: ConfigDefaults::crank_fee_reward(),
196            minimum_deposit_usd: ConfigDefaults::minimum_deposit_usd(),
197            liquifunding_delay_fuzz_seconds: ConfigDefaults::liquifunding_delay_fuzz_seconds(),
198            max_liquidity: ConfigDefaults::max_liquidity(),
199            disable_position_nft_exec: ConfigDefaults::disable_position_nft_exec(),
200            liquidity_cooldown_seconds: ConfigDefaults::liquidity_cooldown_seconds(),
201            exposure_margin_ratio: ConfigDefaults::exposure_margin_ratio(),
202            referral_reward_ratio: ConfigDefaults::referral_reward_ratio(),
203            spot_price,
204            _unused1: None,
205            _unused2: None,
206            _unused3: None,
207            _unused4: None,
208        }
209    }
210
211    /// Ensure that the settings within this [Config] are valid.
212    pub fn validate(&self) -> Result<()> {
213        // note - crank_execs_after_push and mute_events are inherently always valid
214
215        if self.trading_fee_notional_size >= "0.0999".parse().unwrap() {
216            let error_msg = format!("trading_fee_notional_size must be in the range 0 to 0.0999 inclusive ({} is invalid)", self.trading_fee_notional_size);
217            bail!(PerpError::market(ErrorId::Config, error_msg))
218        }
219
220        if self.trading_fee_counter_collateral >= "0.0999".parse().unwrap() {
221            let error_msg = format!("trading_fee_counter_collateral must be in the range 0 to 0.0999 inclusive ({} is invalid)", self.trading_fee_counter_collateral);
222            bail!(PerpError::market(ErrorId::Config, error_msg))
223        }
224
225        if self.crank_execs == 0 {
226            bail!(PerpError::market(
227                ErrorId::Config,
228                "crank_execs_per_batch must be greater than zero"
229            ))
230        }
231
232        if self.max_leverage <= Number::ONE {
233            bail!(PerpError::market(
234                ErrorId::Config,
235                format!(
236                    "max_leverage must be greater than one ({} is invalid)",
237                    self.max_leverage
238                )
239            ))
240        }
241
242        if self.carry_leverage <= Decimal256::one() {
243            bail!(PerpError::market(
244                ErrorId::Config,
245                format!(
246                    "carry_leverage must be greater than one ({} is invalid)",
247                    self.carry_leverage
248                )
249            ))
250        }
251
252        if (self.carry_leverage.into_number() + Number::ONE)? > self.max_leverage {
253            let msg = format!("carry_leverage must be at least one less than max_leverage ({} is invalid, max_leverage is {})",
254                self.carry_leverage,
255                self.max_leverage);
256            bail!(PerpError::market(ErrorId::Config, msg))
257        }
258
259        if self.borrow_fee_rate_max_annualized < self.borrow_fee_rate_min_annualized {
260            let msg = format!("borrow_fee_rate_min_annualized ({}) must be less than borrow_fee_rate_max_annualized ({})",
261                self.borrow_fee_rate_min_annualized,
262                self.borrow_fee_rate_max_annualized);
263            bail!(PerpError::market(ErrorId::Config, msg))
264        }
265
266        if self.protocol_tax >= Decimal256::one() {
267            let msg = format!(
268                "protocol_tax must be less than or equal to 1 ({} is invalid)",
269                self.protocol_tax
270            );
271            bail!(PerpError::market(ErrorId::Config, msg))
272        }
273
274        if self.unstake_period_seconds == 0 {
275            let msg = format!(
276                "unstake period must be greater than 0 ({} is invalid)",
277                self.unstake_period_seconds
278            );
279            bail!(PerpError::market(ErrorId::Config, msg))
280        }
281
282        if Number::from(self.target_utilization) >= Number::ONE {
283            let msg = format!(
284                "Target utilization ratio must be between 0 and 1 exclusive ({} is invalid)",
285                self.target_utilization
286            );
287            bail!(PerpError::market(ErrorId::Config, msg))
288        }
289
290        if Number::from(self.min_xlp_rewards_multiplier) < Number::ONE {
291            let msg = format!(
292                "Min xLP rewards multiplier must be at least 1 ({} is invalid)",
293                self.max_xlp_rewards_multiplier
294            );
295            bail!(PerpError::market(ErrorId::Config, msg))
296        }
297
298        if self.max_xlp_rewards_multiplier < self.min_xlp_rewards_multiplier {
299            let msg = format!(
300                "Max xLP rewards multiplier ({}) must be greater than or equal to the min ({})",
301                self.max_xlp_rewards_multiplier, self.min_xlp_rewards_multiplier
302            );
303            bail!(PerpError::market(ErrorId::Config, msg))
304        }
305
306        if self.crank_fee_charged < self.crank_fee_reward {
307            let msg = format!(
308                "Crank fee charged ({}) must be greater than or equal to the crank fee reward ({})",
309                self.crank_fee_charged, self.crank_fee_reward
310            );
311            bail!(PerpError::market(ErrorId::Config, msg))
312        }
313
314        if self.delta_neutrality_fee_tax > Decimal256::one() {
315            let msg = format!(
316                "Delta neutrality fee tax ({}) must be less than or equal to 1",
317                self.delta_neutrality_fee_tax
318            );
319            bail!(PerpError::market(ErrorId::Config, msg))
320        }
321
322        if self.liquifunding_delay_fuzz_seconds >= self.liquifunding_delay_seconds {
323            let msg = format!("Liquifunding delay fuzz ({}) must be less than or equal to the liquifunding delay ({})",
324                self.liquifunding_delay_fuzz_seconds,
325                self.liquifunding_delay_seconds);
326            bail!(PerpError::market(ErrorId::Config, msg))
327        }
328
329        if self.exposure_margin_ratio >= Decimal256::one() {
330            let msg = format!(
331                "Exposure margin ratio ({}) must be less than 1",
332                self.exposure_margin_ratio
333            );
334            bail!(PerpError::market(ErrorId::Config, msg))
335        }
336
337        if self.referral_reward_ratio >= Decimal256::one() {
338            let msg = format!(
339                "Referral reward ratio ({}) must be less than 1",
340                self.referral_reward_ratio
341            );
342            bail!(PerpError::market(ErrorId::Config, msg))
343        }
344
345        Ok(())
346    }
347}
348
349/// Helper struct to conveniently update [Config]
350///
351/// For each field below, please see the corresponding [Config] field's
352/// documentation.
353#[cw_serde]
354#[allow(missing_docs)]
355#[derive(Default)]
356pub struct ConfigUpdate {
357    pub trading_fee_notional_size: Option<Decimal256>,
358    pub trading_fee_counter_collateral: Option<Decimal256>,
359    pub crank_execs: Option<u32>,
360    pub max_leverage: Option<Number>,
361    pub carry_leverage: Option<Decimal256>,
362    pub funding_rate_sensitivity: Option<Decimal256>,
363    pub funding_rate_max_annualized: Option<Decimal256>,
364    pub borrow_fee_rate_min_annualized: Option<NumberGtZero>,
365    pub borrow_fee_rate_max_annualized: Option<NumberGtZero>,
366    pub mute_events: Option<bool>,
367    pub liquifunding_delay_seconds: Option<u32>,
368    pub protocol_tax: Option<Decimal256>,
369    pub unstake_period_seconds: Option<u32>,
370    pub target_utilization: Option<NumberGtZero>,
371    pub borrow_fee_sensitivity: Option<NumberGtZero>,
372    pub max_xlp_rewards_multiplier: Option<NumberGtZero>,
373    pub min_xlp_rewards_multiplier: Option<NumberGtZero>,
374    pub delta_neutrality_fee_sensitivity: Option<NumberGtZero>,
375    pub delta_neutrality_fee_cap: Option<NumberGtZero>,
376    pub delta_neutrality_fee_tax: Option<Decimal256>,
377    pub crank_fee_charged: Option<Usd>,
378    pub crank_fee_surcharge: Option<Usd>,
379    pub crank_fee_reward: Option<Usd>,
380    pub minimum_deposit_usd: Option<Usd>,
381    pub liquifunding_delay_fuzz_seconds: Option<u32>,
382    pub max_liquidity: Option<MaxLiquidity>,
383    pub disable_position_nft_exec: Option<bool>,
384    pub liquidity_cooldown_seconds: Option<u32>,
385    pub spot_price: Option<SpotPriceConfigInit>,
386    pub exposure_margin_ratio: Option<Decimal256>,
387    pub referral_reward_ratio: Option<Decimal256>,
388}
389#[cfg(feature = "arbitrary")]
390impl<'a> arbitrary::Arbitrary<'a> for ConfigUpdate {
391    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
392        Ok(Self {
393            trading_fee_notional_size: arbitrary_decimal_256_option(u)?,
394            trading_fee_counter_collateral: arbitrary_decimal_256_option(u)?,
395            crank_execs: u.arbitrary()?,
396            max_leverage: u.arbitrary()?,
397            carry_leverage: arbitrary_decimal_256_option(u)?,
398            funding_rate_sensitivity: arbitrary_decimal_256_option(u)?,
399            funding_rate_max_annualized: arbitrary_decimal_256_option(u)?,
400            borrow_fee_rate_min_annualized: u.arbitrary()?,
401            borrow_fee_rate_max_annualized: u.arbitrary()?,
402            mute_events: u.arbitrary()?,
403            liquifunding_delay_seconds: u.arbitrary()?,
404            protocol_tax: arbitrary_decimal_256_option(u)?,
405            unstake_period_seconds: u.arbitrary()?,
406            target_utilization: u.arbitrary()?,
407            borrow_fee_sensitivity: u.arbitrary()?,
408            max_xlp_rewards_multiplier: u.arbitrary()?,
409            min_xlp_rewards_multiplier: u.arbitrary()?,
410            delta_neutrality_fee_sensitivity: u.arbitrary()?,
411            delta_neutrality_fee_cap: u.arbitrary()?,
412            delta_neutrality_fee_tax: arbitrary_decimal_256_option(u)?,
413            crank_fee_charged: u.arbitrary()?,
414            crank_fee_surcharge: u.arbitrary()?,
415            crank_fee_reward: u.arbitrary()?,
416            minimum_deposit_usd: u.arbitrary()?,
417            liquifunding_delay_fuzz_seconds: None,
418            max_liquidity: None,
419            disable_position_nft_exec: None,
420            liquidity_cooldown_seconds: None,
421            exposure_margin_ratio: arbitrary_decimal_256_option(u)?,
422            referral_reward_ratio: None,
423            spot_price: None,
424        })
425    }
426}
427
428impl From<Config> for ConfigUpdate {
429    fn from(src: Config) -> Self {
430        Self {
431            trading_fee_notional_size: Some(src.trading_fee_notional_size),
432            trading_fee_counter_collateral: Some(src.trading_fee_counter_collateral),
433            crank_execs: Some(src.crank_execs),
434            max_leverage: Some(src.max_leverage),
435            carry_leverage: Some(src.carry_leverage),
436            funding_rate_sensitivity: Some(src.funding_rate_sensitivity),
437            funding_rate_max_annualized: Some(src.funding_rate_max_annualized),
438            mute_events: Some(src.mute_events),
439            liquifunding_delay_seconds: Some(src.liquifunding_delay_seconds),
440            protocol_tax: Some(src.protocol_tax),
441            unstake_period_seconds: Some(src.unstake_period_seconds),
442            target_utilization: Some(src.target_utilization),
443            borrow_fee_sensitivity: Some(src.borrow_fee_sensitivity),
444            borrow_fee_rate_min_annualized: Some(src.borrow_fee_rate_min_annualized),
445            borrow_fee_rate_max_annualized: Some(src.borrow_fee_rate_max_annualized),
446            max_xlp_rewards_multiplier: Some(src.max_xlp_rewards_multiplier),
447            min_xlp_rewards_multiplier: Some(src.min_xlp_rewards_multiplier),
448            delta_neutrality_fee_sensitivity: Some(src.delta_neutrality_fee_sensitivity),
449            delta_neutrality_fee_cap: Some(src.delta_neutrality_fee_cap),
450            delta_neutrality_fee_tax: Some(src.delta_neutrality_fee_tax),
451            crank_fee_charged: Some(src.crank_fee_charged),
452            crank_fee_surcharge: Some(src.crank_fee_surcharge),
453            crank_fee_reward: Some(src.crank_fee_reward),
454            minimum_deposit_usd: Some(src.minimum_deposit_usd),
455            liquifunding_delay_fuzz_seconds: Some(src.liquifunding_delay_fuzz_seconds),
456            max_liquidity: Some(src.max_liquidity),
457            disable_position_nft_exec: Some(src.disable_position_nft_exec),
458            liquidity_cooldown_seconds: Some(src.liquidity_cooldown_seconds),
459            exposure_margin_ratio: Some(src.exposure_margin_ratio),
460            referral_reward_ratio: Some(src.referral_reward_ratio),
461            spot_price: Some(src.spot_price.into()),
462        }
463    }
464}