levana_perpswap_cosmos/contracts/market/
spot_price.rs

1//! Spot price data structures
2
3use crate::storage::{NumberGtZero, RawAddr};
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::Addr;
6use pyth_sdk_cw::PriceIdentifier;
7use std::str::FromStr;
8
9/// Spot price config
10#[cw_serde]
11pub enum SpotPriceConfig {
12    /// Manual spot price
13    Manual {
14        /// The admin address for manual spot price updates
15        admin: Addr,
16    },
17    /// External oracle
18    Oracle {
19        /// Pyth configuration, required on chains that use pyth feeds
20        pyth: Option<PythConfig>,
21        /// Stride configuration, required on chains that use stride
22        stride: Option<StrideConfig>,
23        /// sequence of spot price feeds which are composed to generate a single spot price
24        feeds: Vec<SpotPriceFeed>,
25        /// if necessary, sequence of spot price feeds which are composed to generate a single USD spot price
26        feeds_usd: Vec<SpotPriceFeed>,
27        /// How many seconds the publish time of volatile feeds are allowed to diverge from each other
28        ///
29        /// An attacker can, in theory, selectively choose two different publish
30        /// times for a pair of assets and manipulate the combined price. This value allows
31        /// us to say that the publish time cannot diverge by too much. As opposed to age
32        /// tolerance, this allows for latency in getting transactions to land on-chain
33        /// after publish time, and therefore can be a much tighter value.
34        ///
35        /// By default, we use 5 seconds.
36        volatile_diff_seconds: Option<u32>,
37    },
38}
39
40/// Configuration for pyth
41#[cw_serde]
42pub struct PythConfig {
43    /// The address of the pyth oracle contract
44    pub contract_address: Addr,
45    /// Which network to use for the price service
46    /// This isn't used for any internal logic, but clients must use the appropriate
47    /// price service endpoint to match this
48    pub network: PythPriceServiceNetwork,
49}
50
51/// Configuration for stride
52#[cw_serde]
53pub struct StrideConfig {
54    /// The address of the redemption rate contract
55    pub contract_address: Addr,
56}
57
58/// An individual feed used to compose a final spot price
59#[cw_serde]
60pub struct SpotPriceFeed {
61    /// The data for this price feed
62    pub data: SpotPriceFeedData,
63    /// is this price feed inverted
64    pub inverted: bool,
65    /// Is this a volatile feed?
66    ///
67    /// Volatile feeds are expected to have frequent and significant price
68    /// swings. By contrast, a non-volatile feed may be a redemption rate, which will
69    /// slowly update over time. The purpose of volatility is to determine whether
70    /// the publich time for a composite spot price should include the individual feed
71    /// or not. For example, if we have a market like StakedETH_BTC, we would have a
72    /// StakedETH redemption rate, the price of ETH, and the price of BTC. We'd mark ETH
73    /// and BTC as volatile, and the redemption rate as non-volatile. Then the publish
74    /// time would be the earlier of the ETH and BTC publish time.
75    ///
76    /// This field is optional. If omitted, it will use a default based on
77    /// the `data` field, specifically: Pyth and Sei variants are considered volatile,
78    /// Constant, Stride, and Simple are non-volatile.
79    pub volatile: Option<bool>,
80}
81
82/// The data for an individual spot price feed
83#[cw_serde]
84pub enum SpotPriceFeedData {
85    /// Hardcoded value
86    Constant {
87        /// The constant price
88        price: NumberGtZero,
89    },
90    /// Pyth price feeds
91    Pyth {
92        /// The identifier on pyth
93        id: PriceIdentifier,
94        /// price age tolerance, in seconds
95        ///
96        /// We thought about removing this parameter when moving to deferred
97        /// execution. However, this would leave open a potential attack vector of opening
98        /// limit orders or positions, shutting down price updates, and then selectively
99        /// replaying old price updates for favorable triggers.
100        age_tolerance_seconds: u32,
101    },
102    /// Stride liquid staking
103    Stride {
104        /// The IBC denom for the asset
105        denom: String,
106        /// price age tolerance, in seconds
107        age_tolerance_seconds: u32,
108    },
109    /// Native oracle module on the sei chain
110    Sei {
111        /// The denom to use
112        denom: String,
113    },
114    /// Rujira chain
115    Rujira {
116        /// The asset to use
117        asset: String,
118    },
119    /// Simple contract with a QueryMsg::Price call
120    Simple {
121        /// The contract to use
122        contract: Addr,
123        /// price age tolerance, in seconds
124        age_tolerance_seconds: u32,
125    },
126}
127
128/// Which network to use for the price service
129#[cw_serde]
130#[derive(Copy)]
131pub enum PythPriceServiceNetwork {
132    /// Stable CosmWasm
133    ///
134    /// From <https://pyth.network/developers/price-feed-ids#cosmwasm-stable>
135    Stable,
136    /// Edge CosmWasm
137    ///
138    /// From <https://pyth.network/developers/price-feed-ids#cosmwasm-edge>
139    Edge,
140}
141
142impl FromStr for PythPriceServiceNetwork {
143    type Err = anyhow::Error;
144
145    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
146        match s {
147            "stable" => Ok(Self::Stable),
148            "edge" => Ok(Self::Edge),
149            _ => Err(anyhow::anyhow!(
150                "Invalid feed type: {s}. Expected 'stable' or 'edge'"
151            )),
152        }
153    }
154}
155
156/********* Just for config init *********/
157/// Spot price config for initialization messages
158#[cw_serde]
159pub enum SpotPriceConfigInit {
160    /// Manual spot price
161    Manual {
162        /// The admin address for manual spot price updates
163        admin: RawAddr,
164    },
165    /// External oracle
166    Oracle {
167        /// Pyth configuration, required on chains that use pyth feeds
168        pyth: Option<PythConfigInit>,
169        /// Stride configuration, required on chains that use stride feeds
170        stride: Option<StrideConfigInit>,
171        /// sequence of spot price feeds which are composed to generate a single spot price
172        feeds: Vec<SpotPriceFeedInit>,
173        /// if necessary, sequence of spot price feeds which are composed to generate a single USD spot price
174        feeds_usd: Vec<SpotPriceFeedInit>,
175        /// See [SpotPriceConfig::Oracle::volatile_diff_seconds]
176        volatile_diff_seconds: Option<u32>,
177    },
178}
179
180impl From<SpotPriceConfig> for SpotPriceConfigInit {
181    fn from(src: SpotPriceConfig) -> Self {
182        match src {
183            SpotPriceConfig::Manual { admin } => Self::Manual {
184                admin: RawAddr::from(admin),
185            },
186            SpotPriceConfig::Oracle {
187                pyth,
188                stride,
189                feeds,
190                feeds_usd,
191                volatile_diff_seconds,
192            } => Self::Oracle {
193                pyth: pyth.map(|pyth| PythConfigInit {
194                    contract_address: RawAddr::from(pyth.contract_address),
195                    network: pyth.network,
196                }),
197                stride: stride.map(|stride| StrideConfigInit {
198                    contract_address: RawAddr::from(stride.contract_address),
199                }),
200                feeds: feeds.iter().map(|feed| feed.clone().into()).collect(),
201                feeds_usd: feeds_usd
202                    .iter()
203                    .map(|feed_usd| feed_usd.clone().into())
204                    .collect(),
205                volatile_diff_seconds,
206            },
207        }
208    }
209}
210
211/// An individual feed used to compose a final spot price
212#[cw_serde]
213pub struct SpotPriceFeedInit {
214    /// The data for this price feed
215    pub data: SpotPriceFeedDataInit,
216    /// is this price feed inverted
217    pub inverted: bool,
218    /// See [SpotPriceFeed::volatile]
219    pub volatile: Option<bool>,
220}
221impl From<SpotPriceFeed> for SpotPriceFeedInit {
222    fn from(src: SpotPriceFeed) -> Self {
223        Self {
224            data: src.data.into(),
225            inverted: src.inverted,
226            volatile: src.volatile,
227        }
228    }
229}
230
231/// The data for an individual spot price feed
232#[cw_serde]
233pub enum SpotPriceFeedDataInit {
234    /// Hardcoded value
235    Constant {
236        /// The constant price
237        price: NumberGtZero,
238    },
239    /// Pyth price feeds
240    Pyth {
241        /// The identifier on pyth
242        id: PriceIdentifier,
243        /// price age tolerance, in seconds
244        age_tolerance_seconds: u32,
245    },
246    /// Stride liquid staking
247    Stride {
248        /// The IBC denom for the asset
249        denom: String,
250        /// price age tolerance, in seconds
251        age_tolerance_seconds: u32,
252    },
253    /// Native oracle module on the sei chain
254    Sei {
255        /// The denom to use
256        denom: String,
257    },
258    /// Rujira chain
259    Rujira {
260        /// The asset to use
261        asset: String,
262    },
263    /// Simple contract with a QueryMsg::Price call
264    Simple {
265        /// The contract to use
266        contract: RawAddr,
267        /// price age tolerance, in seconds
268        age_tolerance_seconds: u32,
269    },
270}
271impl From<SpotPriceFeedData> for SpotPriceFeedDataInit {
272    fn from(src: SpotPriceFeedData) -> Self {
273        match src {
274            SpotPriceFeedData::Constant { price } => SpotPriceFeedDataInit::Constant { price },
275            SpotPriceFeedData::Pyth {
276                id,
277                age_tolerance_seconds,
278            } => SpotPriceFeedDataInit::Pyth {
279                id,
280                age_tolerance_seconds,
281            },
282            SpotPriceFeedData::Stride {
283                denom,
284                age_tolerance_seconds,
285            } => SpotPriceFeedDataInit::Stride {
286                denom,
287                age_tolerance_seconds,
288            },
289            SpotPriceFeedData::Sei { denom } => SpotPriceFeedDataInit::Sei { denom },
290            SpotPriceFeedData::Rujira { asset } => SpotPriceFeedDataInit::Rujira { asset },
291            SpotPriceFeedData::Simple {
292                contract,
293                age_tolerance_seconds,
294            } => SpotPriceFeedDataInit::Simple {
295                contract: contract.into(),
296                age_tolerance_seconds,
297            },
298        }
299    }
300}
301
302/// Configuration for pyth init messages
303#[cw_serde]
304pub struct PythConfigInit {
305    /// The address of the pyth oracle contract
306    pub contract_address: RawAddr,
307    /// Which network to use for the price service
308    /// This isn't used for any internal logic, but clients must use the appropriate
309    /// price service endpoint to match this
310    pub network: PythPriceServiceNetwork,
311}
312
313/// Configuration for stride
314#[cw_serde]
315pub struct StrideConfigInit {
316    /// The address of the redemption rate contract
317    pub contract_address: RawAddr,
318}
319
320/// Spot price events
321pub mod events {
322    use crate::prelude::*;
323    use cosmwasm_std::Event;
324
325    /// Event emited when a new spot price is added to the protocol.
326    pub struct SpotPriceEvent {
327        /// Timestamp of the update
328        pub timestamp: Timestamp,
329        /// Price of the collateral asset in USD
330        pub price_usd: PriceCollateralInUsd,
331        /// Price of the notional asset in collateral, generated by the protocol
332        pub price_notional: Price,
333        /// Price of the base asset in quote
334        pub price_base: PriceBaseInQuote,
335        /// publish time, if available
336        pub publish_time: Option<Timestamp>,
337        /// publish time, if available
338        pub publish_time_usd: Option<Timestamp>,
339    }
340
341    impl From<SpotPriceEvent> for Event {
342        fn from(src: SpotPriceEvent) -> Self {
343            let mut evt = Event::new("spot-price").add_attributes(vec![
344                ("price-usd", src.price_usd.to_string()),
345                ("price-notional", src.price_notional.to_string()),
346                ("price-base", src.price_base.to_string()),
347                ("time", src.timestamp.to_string()),
348            ]);
349
350            if let Some(publish_time) = src.publish_time {
351                evt = evt.add_attribute("publish-time", publish_time.to_string());
352            }
353            if let Some(publish_time_usd) = src.publish_time_usd {
354                evt = evt.add_attribute("publish-time-usd", publish_time_usd.to_string());
355            }
356
357            evt
358        }
359    }
360    impl TryFrom<Event> for SpotPriceEvent {
361        type Error = anyhow::Error;
362
363        fn try_from(evt: Event) -> anyhow::Result<Self> {
364            Ok(Self {
365                timestamp: evt.timestamp_attr("time")?,
366                price_usd: PriceCollateralInUsd::try_from_number(evt.number_attr("price-usd")?)?,
367                price_notional: Price::try_from_number(evt.number_attr("price-notional")?)?,
368                price_base: PriceBaseInQuote::try_from_number(evt.number_attr("price-base")?)?,
369                publish_time: evt.try_timestamp_attr("publish-time")?,
370                publish_time_usd: evt.try_timestamp_attr("publish-time-usd")?,
371            })
372        }
373    }
374}