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}