1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
//! Spot price data structures

use crate::storage::{NumberGtZero, RawAddr};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
use pyth_sdk_cw::PriceIdentifier;
use std::str::FromStr;

/// Spot price config
#[cw_serde]
pub enum SpotPriceConfig {
    /// Manual spot price
    Manual {
        /// The admin address for manual spot price updates
        admin: Addr,
    },
    /// External oracle
    Oracle {
        /// Pyth configuration, required on chains that use pyth feeds
        pyth: Option<PythConfig>,
        /// Stride configuration, required on chains that use stride
        stride: Option<StrideConfig>,
        /// sequence of spot price feeds which are composed to generate a single spot price
        feeds: Vec<SpotPriceFeed>,
        /// if necessary, sequence of spot price feeds which are composed to generate a single USD spot price
        feeds_usd: Vec<SpotPriceFeed>,
        /// How many seconds the publish time of volatile feeds are allowed to diverge from each other
        ///
        /// An attacker can, in theory, selectively choose two different publish
        /// times for a pair of assets and manipulate the combined price. This value allows
        /// us to say that the publish time cannot diverge by too much. As opposed to age
        /// tolerance, this allows for latency in getting transactions to land on-chain
        /// after publish time, and therefore can be a much tighter value.
        ///
        /// By default, we use 5 seconds.
        volatile_diff_seconds: Option<u32>,
    },
}

/// Configuration for pyth
#[cw_serde]
pub struct PythConfig {
    /// The address of the pyth oracle contract
    pub contract_address: Addr,
    /// Which network to use for the price service
    /// This isn't used for any internal logic, but clients must use the appropriate
    /// price service endpoint to match this
    pub network: PythPriceServiceNetwork,
}

/// Configuration for stride
#[cw_serde]
pub struct StrideConfig {
    /// The address of the redemption rate contract
    pub contract_address: Addr,
}

/// An individual feed used to compose a final spot price
#[cw_serde]
pub struct SpotPriceFeed {
    /// The data for this price feed
    pub data: SpotPriceFeedData,
    /// is this price feed inverted
    pub inverted: bool,
    /// Is this a volatile feed?
    ///
    /// Volatile feeds are expected to have frequent and significant price
    /// swings. By contrast, a non-volatile feed may be a redemption rate, which will
    /// slowly update over time. The purpose of volatility is to determine whether
    /// the publich time for a composite spot price should include the individual feed
    /// or not. For example, if we have a market like StakedETH_BTC, we would have a
    /// StakedETH redemption rate, the price of ETH, and the price of BTC. We'd mark ETH
    /// and BTC as volatile, and the redemption rate as non-volatile. Then the publish
    /// time would be the earlier of the ETH and BTC publish time.
    ///
    /// This field is optional. If omitted, it will use a default based on
    /// the `data` field, specifically: Pyth and Sei variants are considered volatile,
    /// Constant, Stride, and Simple are non-volatile.
    pub volatile: Option<bool>,
}

/// The data for an individual spot price feed
#[cw_serde]
pub enum SpotPriceFeedData {
    /// Hardcoded value
    Constant {
        /// The constant price
        price: NumberGtZero,
    },
    /// Pyth price feeds
    Pyth {
        /// The identifier on pyth
        id: PriceIdentifier,
        /// price age tolerance, in seconds
        ///
        /// We thought about removing this parameter when moving to deferred
        /// execution. However, this would leave open a potential attack vector of opening
        /// limit orders or positions, shutting down price updates, and then selectively
        /// replaying old price updates for favorable triggers.
        age_tolerance_seconds: u32,
    },
    /// Stride liquid staking
    Stride {
        /// The IBC denom for the asset
        denom: String,
        /// price age tolerance, in seconds
        age_tolerance_seconds: u32,
    },
    /// Native oracle module on the sei chain
    Sei {
        /// The denom to use
        denom: String,
    },
    /// Rujira chain
    Rujira {
        /// The asset to use
        asset: String,
    },
    /// Simple contract with a QueryMsg::Price call
    Simple {
        /// The contract to use
        contract: Addr,
        /// price age tolerance, in seconds
        age_tolerance_seconds: u32,
    },
}

/// Which network to use for the price service
#[cw_serde]
#[derive(Copy)]
pub enum PythPriceServiceNetwork {
    /// Stable CosmWasm
    ///
    /// From <https://pyth.network/developers/price-feed-ids#cosmwasm-stable>
    Stable,
    /// Edge CosmWasm
    ///
    /// From <https://pyth.network/developers/price-feed-ids#cosmwasm-edge>
    Edge,
}

impl FromStr for PythPriceServiceNetwork {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "stable" => Ok(Self::Stable),
            "edge" => Ok(Self::Edge),
            _ => Err(anyhow::anyhow!(
                "Invalid feed type: {s}. Expected 'stable' or 'edge'"
            )),
        }
    }
}

/********* Just for config init *********/
/// Spot price config for initialization messages
#[cw_serde]
pub enum SpotPriceConfigInit {
    /// Manual spot price
    Manual {
        /// The admin address for manual spot price updates
        admin: RawAddr,
    },
    /// External oracle
    Oracle {
        /// Pyth configuration, required on chains that use pyth feeds
        pyth: Option<PythConfigInit>,
        /// Stride configuration, required on chains that use stride feeds
        stride: Option<StrideConfigInit>,
        /// sequence of spot price feeds which are composed to generate a single spot price
        feeds: Vec<SpotPriceFeedInit>,
        /// if necessary, sequence of spot price feeds which are composed to generate a single USD spot price
        feeds_usd: Vec<SpotPriceFeedInit>,
        /// See [SpotPriceConfig::Oracle::volatile_diff_seconds]
        volatile_diff_seconds: Option<u32>,
    },
}

impl From<SpotPriceConfig> for SpotPriceConfigInit {
    fn from(src: SpotPriceConfig) -> Self {
        match src {
            SpotPriceConfig::Manual { admin } => Self::Manual {
                admin: RawAddr::from(admin),
            },
            SpotPriceConfig::Oracle {
                pyth,
                stride,
                feeds,
                feeds_usd,
                volatile_diff_seconds,
            } => Self::Oracle {
                pyth: pyth.map(|pyth| PythConfigInit {
                    contract_address: RawAddr::from(pyth.contract_address),
                    network: pyth.network,
                }),
                stride: stride.map(|stride| StrideConfigInit {
                    contract_address: RawAddr::from(stride.contract_address),
                }),
                feeds: feeds.iter().map(|feed| feed.clone().into()).collect(),
                feeds_usd: feeds_usd
                    .iter()
                    .map(|feed_usd| feed_usd.clone().into())
                    .collect(),
                volatile_diff_seconds,
            },
        }
    }
}

/// An individual feed used to compose a final spot price
#[cw_serde]
pub struct SpotPriceFeedInit {
    /// The data for this price feed
    pub data: SpotPriceFeedDataInit,
    /// is this price feed inverted
    pub inverted: bool,
    /// See [SpotPriceFeed::volatile]
    pub volatile: Option<bool>,
}
impl From<SpotPriceFeed> for SpotPriceFeedInit {
    fn from(src: SpotPriceFeed) -> Self {
        Self {
            data: src.data.into(),
            inverted: src.inverted,
            volatile: src.volatile,
        }
    }
}

/// The data for an individual spot price feed
#[cw_serde]
pub enum SpotPriceFeedDataInit {
    /// Hardcoded value
    Constant {
        /// The constant price
        price: NumberGtZero,
    },
    /// Pyth price feeds
    Pyth {
        /// The identifier on pyth
        id: PriceIdentifier,
        /// price age tolerance, in seconds
        age_tolerance_seconds: u32,
    },
    /// Stride liquid staking
    Stride {
        /// The IBC denom for the asset
        denom: String,
        /// price age tolerance, in seconds
        age_tolerance_seconds: u32,
    },
    /// Native oracle module on the sei chain
    Sei {
        /// The denom to use
        denom: String,
    },
    /// Rujira chain
    Rujira {
        /// The asset to use
        asset: String,
    },
    /// Simple contract with a QueryMsg::Price call
    Simple {
        /// The contract to use
        contract: RawAddr,
        /// price age tolerance, in seconds
        age_tolerance_seconds: u32,
    },
}
impl From<SpotPriceFeedData> for SpotPriceFeedDataInit {
    fn from(src: SpotPriceFeedData) -> Self {
        match src {
            SpotPriceFeedData::Constant { price } => SpotPriceFeedDataInit::Constant { price },
            SpotPriceFeedData::Pyth {
                id,
                age_tolerance_seconds,
            } => SpotPriceFeedDataInit::Pyth {
                id,
                age_tolerance_seconds,
            },
            SpotPriceFeedData::Stride {
                denom,
                age_tolerance_seconds,
            } => SpotPriceFeedDataInit::Stride {
                denom,
                age_tolerance_seconds,
            },
            SpotPriceFeedData::Sei { denom } => SpotPriceFeedDataInit::Sei { denom },
            SpotPriceFeedData::Rujira { asset } => SpotPriceFeedDataInit::Rujira { asset },
            SpotPriceFeedData::Simple {
                contract,
                age_tolerance_seconds,
            } => SpotPriceFeedDataInit::Simple {
                contract: contract.into(),
                age_tolerance_seconds,
            },
        }
    }
}

/// Configuration for pyth init messages
#[cw_serde]
pub struct PythConfigInit {
    /// The address of the pyth oracle contract
    pub contract_address: RawAddr,
    /// Which network to use for the price service
    /// This isn't used for any internal logic, but clients must use the appropriate
    /// price service endpoint to match this
    pub network: PythPriceServiceNetwork,
}

/// Configuration for stride
#[cw_serde]
pub struct StrideConfigInit {
    /// The address of the redemption rate contract
    pub contract_address: RawAddr,
}

/// Spot price events
pub mod events {
    use crate::prelude::*;
    use cosmwasm_std::Event;

    /// Event emited when a new spot price is added to the protocol.
    pub struct SpotPriceEvent {
        /// Timestamp of the update
        pub timestamp: Timestamp,
        /// Price of the collateral asset in USD
        pub price_usd: PriceCollateralInUsd,
        /// Price of the notional asset in collateral, generated by the protocol
        pub price_notional: Price,
        /// Price of the base asset in quote
        pub price_base: PriceBaseInQuote,
        /// publish time, if available
        pub publish_time: Option<Timestamp>,
        /// publish time, if available
        pub publish_time_usd: Option<Timestamp>,
    }

    impl From<SpotPriceEvent> for Event {
        fn from(src: SpotPriceEvent) -> Self {
            let mut evt = Event::new("spot-price").add_attributes(vec![
                ("price-usd", src.price_usd.to_string()),
                ("price-notional", src.price_notional.to_string()),
                ("price-base", src.price_base.to_string()),
                ("time", src.timestamp.to_string()),
            ]);

            if let Some(publish_time) = src.publish_time {
                evt = evt.add_attribute("publish-time", publish_time.to_string());
            }
            if let Some(publish_time_usd) = src.publish_time_usd {
                evt = evt.add_attribute("publish-time-usd", publish_time_usd.to_string());
            }

            evt
        }
    }
    impl TryFrom<Event> for SpotPriceEvent {
        type Error = anyhow::Error;

        fn try_from(evt: Event) -> anyhow::Result<Self> {
            Ok(Self {
                timestamp: evt.timestamp_attr("time")?,
                price_usd: PriceCollateralInUsd::try_from_number(evt.number_attr("price-usd")?)?,
                price_notional: Price::try_from_number(evt.number_attr("price-notional")?)?,
                price_base: PriceBaseInQuote::try_from_number(evt.number_attr("price-base")?)?,
                publish_time: evt.try_timestamp_attr("publish-time")?,
                publish_time_usd: evt.try_timestamp_attr("publish-time-usd")?,
            })
        }
    }
}